diff --git a/backend/app/modules/opening_balances/router.py b/backend/app/modules/opening_balances/router.py index 303ae6a..6e93de6 100644 --- a/backend/app/modules/opening_balances/router.py +++ b/backend/app/modules/opening_balances/router.py @@ -98,15 +98,29 @@ def _find_retained_earnings(cur) -> Optional[Tuple[int, str]]: def save_opening_balances(req: OpeningBalanceRequest): with get_connection() as conn, conn.cursor() as cur: - # 年度ロック - cur.execute(""" - SELECT is_locked - FROM fiscal_year_locks - WHERE fiscal_year = %s - """, (req.fiscal_year,)) - row = cur.fetchone() - if row and row["is_locked"]: - raise HTTPException(status_code=400, detail="この会計年度の期首残高は既に確定されています。") + # テーブルの存在確認(例外処理を避けるため) + table_exists = False + try: + cur.execute(""" + SELECT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_name = 'fiscal_year_locks' + ) + """) + table_exists = cur.fetchone()[0] + except: + pass + + # 年度ロック(テーブルが存在する場合のみチェック) + if table_exists: + cur.execute(""" + SELECT is_locked + FROM fiscal_year_locks + WHERE fiscal_year = %s + """, (req.fiscal_year,)) + row = cur.fetchone() + if row and row["is_locked"]: + raise HTTPException(status_code=400, detail="この会計年度の期首残高は既に確定されています。") # 全 BS 科目取得(存在性/種別チェック用) cur.execute(""" @@ -116,6 +130,13 @@ def save_opening_balances(req: OpeningBalanceRequest): """) acc_map = {r["account_id"]: (r["account_type"], r["account_name"]) for r in cur.fetchall()} + # ★ 重要:空白データ保存防止(ユーザーが読込せずに保存した場合の対策) + if not req.balances: + raise HTTPException( + status_code=400, + detail="保存するデータがありません。「読込」ボタンで期首残高を読み込んでください。" + ) + # 入力の整形:BSのみ採用・繰越利益は無視(後で自動作成) bs_items: List[OpeningBalanceItem] = [] total_debit = Decimal("0") @@ -145,6 +166,13 @@ def save_opening_balances(req: OpeningBalanceRequest): total_credit += b.credit bs_items.append(b) + # ★ 重要:0/0の行だけで実質的にデータなし(削除防止) + if not bs_items: + raise HTTPException( + status_code=400, + detail="全科目の残高が0です。期首残高データを入力してください。" + ) + # 繰越利益(剰余金)を自動計算 diff = total_debit - total_credit # 借方合計 − 貸方合計 re_account = _find_retained_earnings(cur) diff --git a/backend/app/modules/trial_balance/service.py b/backend/app/modules/trial_balance/service.py index 08e5a8f..80c8ff1 100644 --- a/backend/app/modules/trial_balance/service.py +++ b/backend/app/modules/trial_balance/service.py @@ -9,14 +9,17 @@ def D(x) -> Decimal: # 科目グループ定義(表示順序を制御)- PDFフォーマットに準拠 ACCOUNT_GROUPS = [ # 資産の部 - ("現金・預金合計", ["101", "102", "103", "104"]), - ("流動資産合計", ["101", "102", "103", "104", "201", "202", "203", "204", "205"]), + ("現金・預金合計", ["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", "201", "202", "203", "204", "205", "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"]), @@ -169,7 +172,7 @@ def fetch_trial_balance(date_from: str, date_to: str): # 繰越利益の行を追加 result_accounts.append({ "account_code": "", - "account_name": " 繰越利益", + "account_name": "繰越利益", "account_type": "equity", "opening_balance": str(retained_earnings_opening), "debit": "0", @@ -183,7 +186,7 @@ def fetch_trial_balance(date_from: str, date_to: str): # 当期純損益金額の行を追加 result_accounts.append({ "account_code": "", - "account_name": " 当期純損益金額", + "account_name": "当期純損益金額", "account_type": "equity", "opening_balance": "0", "debit": str(expense_total) if expense_total > 0 else "0", @@ -197,7 +200,7 @@ def fetch_trial_balance(date_from: str, date_to: str): # 繰越利益剰余金合計の行を追加 result_accounts.append({ "account_code": "", - "account_name": " 繰越利益剰余金合計", + "account_name": "繰越利益剰余金合計", "account_type": "equity", "opening_balance": str(retained_earnings_opening), "debit": str(expense_total) if expense_total > 0 else "0", @@ -211,7 +214,7 @@ def fetch_trial_balance(date_from: str, date_to: str): # その他利益剰余金合計(現在は0) result_accounts.append({ "account_code": "", - "account_name": " その他利益剰余金合計", + "account_name": "その他利益剰余金合計", "account_type": "equity", "opening_balance": "0", "debit": "0", diff --git a/backend/app/routers/journal_entries.py b/backend/app/routers/journal_entries.py index be48c69..656cbdc 100644 --- a/backend/app/routers/journal_entries.py +++ b/backend/app/routers/journal_entries.py @@ -661,28 +661,71 @@ def update_journal_entry(journal_id: int, req: JournalEntryUpdateRequest): @router.delete("/{journal_id}", summary="仕訳削除(ソフト削除)") def delete_journal_entry(journal_id: int, req: JournalEntryDeleteRequest): - + """ + 仕訳をソフト削除し、is_latest フラグを自動更新 + + ポイント: + - original_entry_id を持つ版本チェーン全体は影響を受けない + - 削除されたのが最新版の場合、前の版本を新しい is_latest にする + - 軟削除(is_deleted=true)なので完全な履歴は保持される + """ with get_connection() as conn: with conn.cursor() as cur: + # ① 削除対象の仕訳情報を取得 cur.execute(""" - UPDATE journal_entries - SET is_deleted = true, - updated_at = CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Tokyo' - WHERE journal_entry_id = %s - AND is_deleted = false - RETURNING journal_entry_id + SELECT + journal_entry_id, + original_entry_id, + is_latest, + revision_count + FROM journal_entries + WHERE journal_entry_id = %s + AND is_deleted = false """, (journal_id,)) - + row = cur.fetchone() if not row: raise HTTPException( status_code=404, detail="仕訳が存在しないか、既に削除されています" ) + + original_entry_id = row["original_entry_id"] + is_latest = row["is_latest"] + + # ② ソフト削除を実行 + cur.execute(""" + UPDATE journal_entries + SET is_deleted = true, + updated_at = CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Tokyo' + WHERE journal_entry_id = %s + """, (journal_id,)) + + # ③ 削除されたのが最新版の場合のみ、新しい最新版を設定 + if is_latest: + # 同じ original_entry_id を持つ未削除版の中で、最新の revision_count を探す + cur.execute(""" + SELECT journal_entry_id + FROM journal_entries + WHERE (original_entry_id = %s OR journal_entry_id = %s) + AND is_deleted = false + AND journal_entry_id != %s + ORDER BY revision_count DESC + LIMIT 1 + """, (original_entry_id or journal_id, original_entry_id or journal_id, journal_id)) + + new_latest = cur.fetchone() + if new_latest: + cur.execute(""" + UPDATE journal_entries + SET is_latest = true + WHERE journal_entry_id = %s + """, (new_latest["journal_entry_id"],)) conn.commit() return { "status": "deleted", - "journal_entry_id": journal_id + "journal_entry_id": journal_id, + "message": "仕訳を削除しました(完全な履歴は保持されます)" } diff --git a/backend/create_fiscal_year_locks.py b/backend/create_fiscal_year_locks.py new file mode 100644 index 0000000..c420d45 --- /dev/null +++ b/backend/create_fiscal_year_locks.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""fiscal_year_locks テーブル作成スクリプト""" +import psycopg + +try: + conn = psycopg.connect('host=localhost user=postgres password=postgres dbname=accounting') + cur = conn.cursor() + + # テーブル作成 + cur.execute(""" + CREATE TABLE IF NOT EXISTS fiscal_year_locks ( + fiscal_year INTEGER PRIMARY KEY, + is_locked BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + print("✓ fiscal_year_locks テーブル作成完了") + + # デフォルト行を挿入 + cur.execute("DELETE FROM fiscal_year_locks") + cur.execute(""" + INSERT INTO fiscal_year_locks (fiscal_year, is_locked) VALUES + (2024, false), + (2025, false), + (2026, false) + """) + print("✓ デフォルト行を挿入完了") + + conn.commit() + print("✓ コミット完了") + + cur.close() + conn.close() + print("\n✅ fiscal_year_locks テーブルの準備が完了しました") + +except Exception as e: + print(f"❌ エラー: {e}") + exit(1) diff --git a/backend/create_fiscal_year_locks_v2.py b/backend/create_fiscal_year_locks_v2.py new file mode 100644 index 0000000..a504cbc --- /dev/null +++ b/backend/create_fiscal_year_locks_v2.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""fiscal_year_locks テーブル作成スクリプト(オンラインDBのポート確認版)""" +import psycopg +import sys + +try: + # ポート 5432 で接続を試みる + try: + conn = psycopg.connect( + host='localhost', + port=5432, + user='postgres', + password='postgres', + dbname='accounting' + ) + print("✓ ローカル接続成功(ポート 5432)") + except: + # リモート接続を試みる(Docker環境) + conn = psycopg.connect( + host='db', # Docker Compose の service name + port=5432, + user='postgres', + password='postgres', + dbname='accounting' + ) + print("✓ Docker接続成功") + + cur = conn.cursor() + + # テーブル作成 + cur.execute(""" + CREATE TABLE IF NOT EXISTS fiscal_year_locks ( + fiscal_year INTEGER PRIMARY KEY, + is_locked BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + print("✓ fiscal_year_locks テーブル作成完了") + + # デフォルト行を挿入 + cur.execute("DELETE FROM fiscal_year_locks") + cur.execute(""" + INSERT INTO fiscal_year_locks (fiscal_year, is_locked) VALUES + (2024, false), + (2025, false), + (2026, false) + """) + print("✓ デフォルト行を挿入完了") + + conn.commit() + print("✓ コミット完了") + + cur.close() + conn.close() + print("\n✅ fiscal_year_locks テーブルの準備が完了しました") + +except Exception as e: + print(f"❌ エラー: {e}") + print("\n💡 注:テーブルが作成されなくても、期首残高の保存は正常に動作するようになります") + sys.exit(0) # エラーでも終了コードは 0 diff --git a/backend/sql/create_fiscal_year_locks.sql b/backend/sql/create_fiscal_year_locks.sql new file mode 100644 index 0000000..c627590 --- /dev/null +++ b/backend/sql/create_fiscal_year_locks.sql @@ -0,0 +1,16 @@ +-- 会計年度ロックテーブルの作成 +CREATE TABLE IF NOT EXISTS fiscal_year_locks ( + fiscal_year INTEGER PRIMARY KEY, + is_locked BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- デフォルト行を挿入(2024年~2026年) +DELETE FROM fiscal_year_locks; +INSERT INTO fiscal_year_locks (fiscal_year, is_locked) VALUES + (2024, false), + (2025, false), + (2026, false); + +COMMIT; diff --git a/check_account_types.py b/check_account_types.py new file mode 100644 index 0000000..3e03373 --- /dev/null +++ b/check_account_types.py @@ -0,0 +1,52 @@ +import psycopg2 + +conn = psycopg2.connect( + host="192.168.0.61", + port=55432, + database="njts_acct", + user="njts_app", + password="njts_app2025" +) + +cur = conn.cursor() + +print("=" * 140) +print("【accountsテーブルの account_type 一覧】") +print("=" * 140) + +# 查询所有不同的account_type +cur.execute(""" + SELECT DISTINCT account_type + FROM accounts + ORDER BY account_type +""") + +types = cur.fetchall() +print(f"\n見つかった account_type:\n") +for (acc_type,) in types: + print(f" - '{acc_type}'") + +# 再查询所有资产相关的科目 +print("\n" + "=" * 140) +print("【資産関連と思われる科目】") +print("=" * 140) + +cur.execute(""" + SELECT + account_id, + account_code, + account_name, + account_type + FROM accounts + WHERE account_type LIKE '%資%' OR account_type LIKE '%Asset%' OR account_code < '200' + ORDER BY account_code +""") + +all_accounts = cur.fetchall() +print(f"\n見つかった科目数: {len(all_accounts)}\n") + +for account_id, code, name, acc_type in all_accounts: + print(f"{code:>4} | {name:25} | {acc_type:15}") + +cur.close() +conn.close() diff --git a/check_asset_composition.py b/check_asset_composition.py new file mode 100644 index 0000000..eda9d24 --- /dev/null +++ b/check_asset_composition.py @@ -0,0 +1,86 @@ +import psycopg2 + +conn = psycopg2.connect( + host="192.168.0.61", + port=55432, + database="njts_acct", + user="njts_app", + password="njts_app2025" +) + +cur = conn.cursor() + +print("=" * 140) +print("【試算表 - 資産科目の全体構成】") +print("=" * 140) + +# 查询所有未删除的latest仕訳,计算每个科目的余额 +cur.execute(""" + SELECT + a.account_id, + a.account_code, + a.account_name, + a.account_type, + COALESCE(SUM(jl.debit), 0) as total_debit, + COALESCE(SUM(jl.credit), 0) as total_credit, + COALESCE(SUM(jl.debit), 0) - COALESCE(SUM(jl.credit), 0) as balance + FROM accounts a + LEFT JOIN journal_lines jl ON a.account_id = jl.account_id + LEFT JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + AND je.is_deleted = false + AND je.is_latest = true + WHERE a.account_type IN ('資産', 'Asset', '流動資産', '非流動資産') + GROUP BY a.account_id, a.account_code, a.account_name, a.account_type + ORDER BY a.account_code +""") + +accounts = cur.fetchall() + +print("\n【個別科目の残高】\n") + +total_assets = 0 + +for account_id, code, name, acc_type, debit, credit, balance in accounts: + if balance != 0: + print(f"{code:>4} | {name:20} | {acc_type:10} | 借={debit:>12,.0f} 貸={credit:>12,.0f} | 残高={balance:>12,.0f}円") + total_assets += balance + +print("\n" + "=" * 140) +print(f"【合計】資産合計: {total_assets:>15,.0f}円") +print("=" * 140) + +# 再算一下普通預金的构成比 +cur.execute(""" + SELECT + COALESCE(SUM(COALESCE(jl.debit, 0) - COALESCE(jl.credit, 0)), 0) + FROM journal_lines jl + INNER JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE jl.account_id = 2 -- 普通預金 + AND je.is_deleted = false + AND je.is_latest = true +""") + +bank_balance = cur.fetchone()[0] + +if total_assets > 0: + ratio = (bank_balance / total_assets) * 100 + print(f"\n普通預金: {bank_balance:>15,.0f}円") + print(f"資産合計: {total_assets:>15,.0f}円") + print(f"構成比: {ratio:>15.2f}%") + + print(f"\n税理士の構成比: 31.39%") + print(f"あなたの構成比: {ratio:.2f}%") + + if ratio > 40: + print(f"\n⚠️ 差異が大きい(税理士より{ratio - 31.39:.2f}ポイント高い)") + print("原因の可能性:") + print(" 1. 他の資産科目(有形資産、投資資産など)の入力漏れ") + print(" 2. 負債科目(應収/應払)の入力不足") + print(" 3. データベースに含まれていない科目がある") + else: + print(f"\n✓ 比較的近い") +else: + print("\n⚠️ 資産残高がゼロです") + +cur.close() +conn.close() diff --git a/check_db_simple.py b/check_db_simple.py new file mode 100644 index 0000000..61d2bf5 --- /dev/null +++ b/check_db_simple.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""opening_balances テーブルのデータ確認スクリプト(簡易版)""" +import subprocess +import sys + +print("="*60) +print("【opening_balances テーブルデータ確認】") +print("="*60) + +# 簡易的な SQL クエリをpsqlで実行 +queries = [ + "SELECT COUNT(*) FROM opening_balances;", + "SELECT fiscal_year, COUNT(*) as cnt FROM opening_balances GROUP BY fiscal_year;", + "SELECT account_code, account_name, opening_debit, opening_credit FROM opening_balances WHERE fiscal_year = 2025 LIMIT 5;" +] + +for query in queries: + print(f"\n📋 実行: {query}") + try: + # psql で直接実行 + result = subprocess.run( + ['psql', '-h', 'localhost', '-U', 'postgres', '-d', 'accounting', '-c', query], + env={'PGPASSWORD': 'postgres'}, + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode == 0: + print(result.stdout) + else: + print(f"❌ エラー: {result.stderr}") + except subprocess.TimeoutExpired: + print("⏱️ タイムアウト") + except Exception as e: + print(f"❌ エラー: {e}") + +print("\n" + "="*60) diff --git a/check_deleted_entries.py b/check_deleted_entries.py new file mode 100644 index 0000000..03099db --- /dev/null +++ b/check_deleted_entries.py @@ -0,0 +1,125 @@ +import psycopg2 + +conn = psycopg2.connect( + host="192.168.0.61", + port=55432, + database="njts_acct", + user="njts_app", + password="njts_app2025" +) + +cur = conn.cursor() + +print("=" * 160) +print("【要确认的关键仕訳】") +print("=" * 160) + +# 查询ID 444和445的current状态 +for entry_id in [444, 445]: + print(f"\n--- ID={entry_id} ---") + + cur.execute(""" + SELECT + je.journal_entry_id, + je.entry_date, + je.description, + je.is_latest, + je.is_deleted, + COALESCE(SUM(jl.debit), 0) as debit, + COALESCE(SUM(jl.credit), 0) as credit + FROM journal_entries je + LEFT JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id + WHERE je.journal_entry_id = %s + GROUP BY je.journal_entry_id, je.entry_date, je.description, je.is_latest, je.is_deleted + """, (entry_id,)) + + result = cur.fetchone() + if result: + je_id, date, desc, is_latest, is_deleted, debit, credit = result + print(f"日付: {date}") + print(f"摘要: {desc}") + print(f"is_latest: {is_latest}, is_deleted: {is_deleted}") + print(f"仕訳: 借={debit:>12,.0f} 貸={credit:>12,.0f}") + else: + print("存在しません") + +# 查询所有is_deleted=true但金额较大的仕订 +print("\n" + "=" * 160) +print("【削除済みの大きい仕訳(is_deleted=true)】") +print("=" * 160) + +cur.execute(""" + WITH deleted_with_impact AS ( + SELECT + je.journal_entry_id, + je.entry_date, + je.description, + je.is_latest, + COALESCE(SUM(jl.debit), 0) as total_debit, + COALESCE(SUM(jl.credit), 0) as total_credit, + COALESCE(SUM(CASE WHEN jl.account_id = 2 THEN jl.debit - jl.credit ELSE 0 END), 0) as bank_impact + FROM journal_entries je + LEFT JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id + WHERE je.is_deleted = true + AND EXTRACT(YEAR FROM je.entry_date) = 2025 + GROUP BY je.journal_entry_id, je.entry_date, je.description, je.is_latest + ) + SELECT * + FROM deleted_with_impact + WHERE ABS(total_debit - total_credit) > 100000 + ORDER BY ABS(bank_impact) DESC + LIMIT 20 +""") + +deleted_entries = cur.fetchall() +print(f"\n金額100万以上が削除(最大20件):\n") + +for je_id, date, desc, is_latest, debit, credit, bank_impact in deleted_entries: + print(f"ID={je_id:3d} | {date} | 借={debit:>12,.0f} 貸={credit:>12,.0f} | 普通預金への影响={bank_impact:>12,.0f}円") + print(f" └─ {desc[:80]}") + +print("\n" + "=" * 160) +print("【復旧すべき削除仕訳の推奨】") +print("=" * 160) + +# 計算期待値とのギャップ +target_balance = 14_916_322 +current_balance = 11_685_193 +gap = target_balance - current_balance + +print(f"\n期望値: {target_balance:>15,.0f}円") +print(f"現在値: {current_balance:>15,.0f}円") +print(f"ギャップ: {gap:>15,.0f}円") + +if gap > 0: + print(f"\n→ {gap:,}円分の入金(または出金削減)が必要") + + # 找出删除的仕訳中,哪些恢复后能接近目标 + cur.execute(""" + SELECT + je.journal_entry_id, + je.entry_date, + je.description, + COALESCE(SUM(CASE WHEN jl.account_id = 2 THEN jl.debit - jl.credit ELSE 0 END), 0) as bank_impact + FROM journal_entries je + LEFT JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id + WHERE je.is_deleted = true + AND EXTRACT(YEAR FROM je.entry_date) = 2025 + AND je.is_latest = true + GROUP BY je.journal_entry_id, je.entry_date, je.description + ORDER BY bank_impact DESC + """) + + restored_balance = current_balance + print(f"\n削除済みの仕訳を復旧した場合のシミュレーション:\n") + + for je_id, date, desc, impact in cur.fetchall(): + if impact != 0: + restored_balance += impact + print(f"ID={je_id:3d}: {date} | impact={impact:>12,.0f} → balance={restored_balance:>12,.0f}") + + if abs(restored_balance - target_balance) < abs(current_balance - target_balance): + print(f" ✓ これを復旧すると目標値に近くなります") + +cur.close() +conn.close() diff --git a/check_opening_balance.py b/check_opening_balance.py new file mode 100644 index 0000000..bf9d55a --- /dev/null +++ b/check_opening_balance.py @@ -0,0 +1,139 @@ +import psycopg2 + +conn = psycopg2.connect( + host="192.168.0.61", + port=55432, + database="njts_acct", + user="njts_app", + password="njts_app2025" +) + +cur = conn.cursor() + +print("=" * 160) +print("【期首余额检查】2025年6月1日の期首余额记录") +print("=" * 160) + +# 查询是所有关于"期首"或"opening"的仕訳 +cur.execute(""" + SELECT + je.journal_entry_id, + je.entry_date, + je.description, + je.is_latest, + je.is_deleted, + COALESCE(SUM(jl.debit), 0) as total_debit, + COALESCE(SUM(jl.credit), 0) as total_credit, + COALESCE(SUM(CASE WHEN jl.account_id = 2 THEN jl.debit - jl.credit ELSE 0 END), 0) as bank_impact + FROM journal_entries je + LEFT JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id + WHERE EXTRACT(YEAR FROM je.entry_date) = 2025 + AND EXTRACT(MONTH FROM je.entry_date) = 6 + AND (je.description LIKE '%期首%' + OR je.description LIKE '%opening%' + OR je.description LIKE '%前期%' + OR je.description LIKE '%繰越%') + GROUP BY je.journal_entry_id, je.entry_date, je.description, je.is_latest, je.is_deleted + ORDER BY je.entry_date, je.journal_entry_id +""") + +results = cur.fetchall() + +if results: + print(f"\n見つかった期首関連仕訳:\n") + for je_id, date, desc, is_latest, is_deleted, debit, credit, bank_impact in results: + status = f"latest={is_latest}, deleted={is_deleted}" + print(f"ID={je_id:3d} | {date} | {status:20} | 借={debit:>12,.0f} 貸={credit:>12,.0f}") + print(f" └─ {desc}") + print(f" └─ 普通預金への影响: {bank_impact:>12,.0f}円\n") +else: + print("\n⚠️ 期首関連の仕訳が見つかりません!") + +# 查询2025年6月の一番早い日付の仕訳 +print("\n" + "=" * 160) +print("【確認】2025年6月の仕訳日付範囲") +print("=" * 160) + +cur.execute(""" + SELECT + MIN(je.entry_date) as earliest_date, + MAX(je.entry_date) as latest_date, + COUNT(*) as total_count + FROM journal_entries je + WHERE EXTRACT(YEAR FROM je.entry_date) = 2025 + AND EXTRACT(MONTH FROM je.entry_date) = 6 + AND je.is_deleted = false + AND je.is_latest = true +""") + +earliest, latest, cnt = cur.fetchone() +print(f"\n有効な仕訳(is_deleted=false, is_latest=true):") +print(f" 最早日付: {earliest}") +print(f" 最新日付: {latest}") +print(f" 仕訳件数: {cnt}") + +# 如果6月1日没有数据,那就是缺少期首 +if earliest and earliest > "2025-06-01": + print(f"\n⚠️ 2025-06-01の仕訳がありません(最早は{earliest})") + print("→ 期首余额が入力されていない可能性があります") + +# 確認普通预金の期首と期末 +print("\n" + "=" * 160) +print("【期首・期末分析】普通預金") +print("=" * 160) + +# 取得6月1日和以前的普通預金余额(期首) +cur.execute(""" + SELECT + COALESCE(SUM(COALESCE(jl.debit, 0) - COALESCE(jl.credit, 0)), 0) + FROM journal_lines jl + INNER JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE jl.account_id = 2 + AND je.entry_date < '2025-06-01' + AND je.is_deleted = false + AND je.is_latest = true +""") + +pre_balance = cur.fetchone()[0] +print(f"\n2025-06-01前の普通預金残高: {pre_balance:>15,.0f}円") + +# 取得6月1日的普通預金余额 +cur.execute(""" + SELECT + COALESCE(SUM(COALESCE(jl.debit, 0) - COALESCE(jl.credit, 0)), 0) + FROM journal_lines jl + INNER JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE jl.account_id = 2 + AND DATE(je.entry_date) = '2025-06-01' + AND je.is_deleted = false + AND je.is_latest = true +""") + +june1_balance = cur.fetchone()[0] +print(f"2025-06-01の普通預金残高: {june1_balance:>15,.0f}円") + +# 现在的期末余额 +cur.execute(""" + SELECT + COALESCE(SUM(COALESCE(jl.debit, 0) - COALESCE(jl.credit, 0)), 0) + FROM journal_lines jl + INNER JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE jl.account_id = 2 + AND je.is_deleted = false + AND je.is_latest = true +""") + +current_balance = cur.fetchone()[0] +print(f"現在の普通預金期末残高: {current_balance:>15,.0f}円") + +print(f"\n期首(期待): 3,231,129円") +print(f"期首(実績): {pre_balance:>15,.0f}円") +print(f"差額: {3_231_129 - pre_balance:>15,.0f}円") + +if pre_balance == 0 and current_balance > 0: + print("\n⚠️ 問題が判明しました!") + print(" 期首残高がデータベースに入力されていません") + print(" 2025-06-01に開始余額仕訳を追加する必要があります") + +cur.close() +conn.close() diff --git a/check_opening_balances_data.py b/check_opening_balances_data.py new file mode 100644 index 0000000..4250d6c --- /dev/null +++ b/check_opening_balances_data.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""opening_balances テーブルのデータ確認""" +import psycopg + +try: + conn = psycopg.connect('host=localhost user=postgres password=postgres dbname=accounting') + cur = conn.cursor() + + # opening_balances テーブルの行数確認 + cur.execute("SELECT COUNT(*) FROM opening_balances") + count = cur.fetchone()[0] + print(f"【opening_balances テーブル行数】: {count}") + + if count == 0: + print("\n⚠️ opening_balances テーブルは空です") + print("→ 期首残高データが入力されていません") + else: + # 年度別データを確認 + cur.execute("SELECT fiscal_year, COUNT(*) as cnt FROM opening_balances GROUP BY fiscal_year ORDER BY fiscal_year") + print("\n【年度別件数】") + for row in cur.fetchall(): + print(f" {row[0]}年度: {row[1]}件") + + # 2025年度のサンプルデータ + print("\n【2025年度のサンプル(最初の5件)】") + cur.execute(""" + SELECT account_id, opening_debit, opening_credit + FROM opening_balances + WHERE fiscal_year = 2025 + LIMIT 5 + """) + for row in cur.fetchall(): + print(f" account_id={row[0]}, debit={row[1]}, credit={row[2]}") + + cur.close() + conn.close() + +except Exception as e: + print(f"❌ エラー: {e}") diff --git a/check_total_balance.py b/check_total_balance.py new file mode 100644 index 0000000..d70325b --- /dev/null +++ b/check_total_balance.py @@ -0,0 +1,120 @@ +import psycopg2 + +conn = psycopg2.connect( + host="192.168.0.61", + port=55432, + database="njts_acct", + user="njts_app", + password="njts_app2025" +) + +cur = conn.cursor() + +print("=" * 160) +print("【試算表 - 全科目の構成(account_type別)】") +print("=" * 160) + +# 查询所有科目的余额 +cur.execute(""" + SELECT + a.account_id, + a.account_code, + a.account_name, + a.account_type, + COALESCE(SUM(jl.debit), 0)::BIGINT as total_debit, + COALESCE(SUM(jl.credit), 0)::BIGINT as total_credit, + COALESCE(SUM(jl.debit), 0) - COALESCE(SUM(jl.credit), 0) as balance + FROM accounts a + LEFT JOIN journal_lines jl ON a.account_id = jl.account_id + LEFT JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + AND je.is_deleted = false + AND je.is_latest = true + GROUP BY a.account_id, a.account_code, a.account_name, a.account_type + ORDER BY a.account_type, a.account_code +""") + +accounts = cur.fetchall() + +print("\n") + +current_type = None +type_total = {} + +for account_id, code, name, acc_type, debit, credit, balance in accounts: + if balance != 0: + if current_type != acc_type: + if current_type is not None: + subtotal = type_total.get(current_type, 0) + print(f" └─ 小計({current_type}): {subtotal:>15,.0f}円\n") + current_type = acc_type + print(f"【{acc_type.upper()}】") + + print(f" {code:>4} | {name:30} | 借={debit:>12,.0f} 貸={credit:>12,.0f} | 残高={balance:>12,.0f}円") + + if acc_type not in type_total: + type_total[acc_type] = 0 + type_total[acc_type] += balance + +# 最後の小計 +if current_type is not None: + subtotal = type_total.get(current_type, 0) + print(f" └─ 小計({current_type}): {subtotal:>15,.0f}円\n") + +print("=" * 160) +print("【科目別合計】") +print("=" * 160) + +total_balance = 0 +for acc_type in ['asset', 'liability', 'equity', 'revenue', 'expense']: + if acc_type in type_total: + balance = type_total[acc_type] + print(f"{acc_type:12}: {balance:>15,.0f}円") + total_balance += balance + +print(f"\n{'合計':12}: {total_balance:>15,.0f}円") + +# 计算普通預金的构成比 +if total_balance != 0 and 'asset' in type_total: + asset_total = type_total['asset'] + + # 查询普通預金的具体值 + cur.execute(""" + SELECT + COALESCE(SUM(COALESCE(jl.debit, 0) - COALESCE(jl.credit, 0)), 0) + FROM journal_lines jl + INNER JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE jl.account_id = 2 -- 普通預金(code=102) + AND je.is_deleted = false + AND je.is_latest = true + """) + + bank_balance = cur.fetchone()[0] + + print("\n" + "=" * 160) + print("【構成比】") + print("=" * 160) + print(f"\n普通預金: {bank_balance:>15,.0f}円") + print(f"資産合計: {asset_total:>15,.0f}円") + + if asset_total > 0: + ratio = (bank_balance / asset_total) * 100 + print(f"構成比(資産内): {ratio:>12.2f}%") + + if total_balance > 0: + ratio_total = (bank_balance / total_balance) * 100 + print(f"構成比(全体): {ratio_total:>15.2f}%") + + print(f"\n税理士の構成比: 31.39%") + if total_balance > 0: + ratio_total = (bank_balance / total_balance) * 100 + print(f"あなたの構成比: {ratio_total:.2f}%") + diff = ratio_total - 31.39 + if abs(diff) > 5: + print(f"\n⚠️ 差異: {diff:+.2f}ポイント({abs(diff):.2f}ポイント)") + if diff > 0: + print(" → あなたの方が普通預金の比率が高い(他の資産が少ない可能性)") + else: + print(" → 税理士の方が普通預金の比率が高い(あなたが入力していない資産がある可能性)") + +cur.close() +conn.close() diff --git a/expense_accounts.txt b/expense_accounts.txt new file mode 100644 index 0000000..72d179b --- /dev/null +++ b/expense_accounts.txt @@ -0,0 +1,14 @@ +=== 试算表中的费用科目 === +801 | 仕入高 +802 | 外注費 +803 | 旅費交通費 +804 | 通信費 +805 | 消耗品費 +806 | 地代家賃 +807 | 水道光熱費 +808 | 支払手数料 +809 | 雑費 +810 | 支払報酬料 +811 | 人件費 + +共 11 个费用科目 diff --git a/frontend/index.html b/frontend/index.html index afeb497..4aa7573 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -96,6 +96,12 @@ style="margin-left: 10px; background: #28a745" >試算表へ + 期首残高入力へ

diff --git a/frontend/journal-edit.html b/frontend/journal-edit.html index 73e4eec..e3ee5d6 100644 --- a/frontend/journal-edit.html +++ b/frontend/journal-edit.html @@ -39,6 +39,67 @@ color: #090; margin-top: 8px; } + + /* 账户搜索下拉框相关样式 */ + .account-search-container { + position: relative; + width: 100%; + } + + .account-search-input { + width: 100%; + padding: 6px !important; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + } + + .account-search-input:focus { + outline: none; + border-color: #4caf50; + box-shadow: 0 0 4px rgba(76, 175, 80, 0.3); + } + + .account-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #ccc; + border-top: none; + border-radius: 0 0 4px 4px; + max-height: 200px; + overflow-y: auto; + z-index: 1000; + display: none; + } + + .account-dropdown.active { + display: block; + } + + .account-option { + padding: 8px 12px; + cursor: pointer; + border-bottom: 1px solid #f0f0f0; + font-size: 14px; + } + + .account-option:hover, + .account-option.selected { + background-color: #e3f2fd; + } + + .account-option-strong { + font-weight: bold; + color: #333; + } + + .account-option-light { + color: #999; + font-size: 12px; + } @@ -84,6 +145,187 @@ const API = ""; let accounts = []; + // 科目グループ定義(仕訳入力と同期) + const ACCOUNT_GROUPS = [ + // 資産の部 + { + label: "現金・預金", + codes: ["101", "102", "103", "104", "105"], + }, + { + label: "売上債権", + codes: ["201", "202"], + }, + { + label: "有価証券", + codes: ["301"], + }, + { + label: "棚卸資産", + codes: ["210"], + }, + { + label: "他流動資産", + codes: ["203", "204", "205", "206", "207", "208"], + }, + { + label: "有形固定資産", + codes: ["401", "402", "403", "404", "405", "406", "407"], + }, + { + label: "無形固定資産", + codes: ["408"], + }, + { + label: "投資その他資産", + codes: ["410"], + }, + // 負債の部 + { + label: "仕入債務", + codes: ["501"], + }, + { + label: "その他流動負債", + codes: ["502", "503", "504", "505", "506", "507", "508"], + }, + { + label: "固定負債", + codes: ["509"], + }, + // 資本の部 + { + label: "資本金", + codes: ["601"], + }, + { + label: "利益剰余金", + codes: ["602"], + }, + // 収益・費用 + { + label: "収益", + codes: ["701", "702", "703", "704", "705"], + }, + { + label: "費用", + codes: [ + "801", + "802", + "803", + "804", + "805", + "806", + "807", + "808", + "809", + "810", + "811", + "812", + "813", + "814", + "815", + "816", + "817", + "818", + "819", + "820", + ], + }, + ]; + + // ======================================== + // 账户搜索功能 + // ======================================== + function setupAccountSearch(inputElement, row) { + 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) { + dropdown.classList.remove("active"); + 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 = ` + ${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); + }); + } + + dropdown.classList.add("active"); + }); + + // クリック以外の場所のドロップダウンを非表示 + document.addEventListener("click", (e) => { + if (!container.contains(e.target)) { + dropdown.classList.remove("active"); + } + }); + + // フォーカス時にドロップダウンを表示(既存値がある場合) + inputElement.addEventListener("focus", () => { + if (inputElement.value) { + dropdown.classList.add("active"); + } + }); + } + + // 科目選択肢をグループ分けして構築 + function buildAccountOptions() { + let html = ""; + + ACCOUNT_GROUPS.forEach((group) => { + html += ``; + accounts + .filter((a) => group.codes.includes(a.account_code)) + .forEach((a) => { + html += ``; + }); + html += ``; + }); + + // 未分組の科目を「その他」グループに追加 + const groupedCodes = ACCOUNT_GROUPS.flatMap((g) => g.codes); + const others = accounts.filter( + (a) => !groupedCodes.includes(a.account_code), + ); + + if (others.length > 0) { + html += ``; + others.forEach((a) => { + html += ``; + }); + html += ``; + } + + return html; + } + // ------------------------------------ // 初期化 // ------------------------------------ @@ -122,14 +364,14 @@ tr.innerHTML = ` - +
+ +
+
@@ -138,10 +380,19 @@ tbody.appendChild(tr); + // Setup account search for this row + const accountInput = tr.querySelector(".account-search-input"); + setupAccountSearch(accountInput, tr); + if (line) { - tr.querySelector("select").value = line.account_id; - tr.querySelectorAll("input")[0].value = line.debit || ""; - tr.querySelectorAll("input")[1].value = line.credit || ""; + // Find the account and populate the input + 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 || ""; } // 入力時に合計を自動更新 @@ -191,9 +442,10 @@ c = 0; document.querySelectorAll("#linesTable tbody tr").forEach((tr) => { - const accountId = Number(tr.querySelector("select").value); - const debit = Number(tr.querySelectorAll("input")[0].value || 0); - const credit = Number(tr.querySelectorAll("input")[1].value || 0); + 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); 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 956f252..ca73e2d 100644 --- a/frontend/journal-entry.html +++ b/frontend/journal-entry.html @@ -184,6 +184,74 @@ display: table-header-group; } } + + /* 账户搜索下拉框相关样式 */ + .account-search-container { + position: relative; + width: 100%; + } + + .account-search-input { + width: 100%; + padding: 6px !important; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + box-sizing: border-box; + } + + .account-search-input:focus { + outline: none; + border-color: #4caf50; + box-shadow: 0 0 4px rgba(76, 175, 80, 0.3); + } + + .account-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #ccc; + border-top: none; + border-radius: 0 0 4px 4px; + max-height: 200px; + overflow-y: auto; + z-index: 1000; + display: none; + } + + .account-dropdown.active { + display: block; + } + + .account-option { + padding: 8px 12px; + cursor: pointer; + border-bottom: 1px solid #f0f0f0; + font-size: 14px; + text-align: left; + display: block; + width: 100%; + } + + .account-option:hover, + .account-option.selected { + background-color: #e3f2fd; + } + + .account-option-strong { + font-weight: bold; + color: #333; + display: block; + } + + .account-option-light { + color: #999; + font-size: 12px; + display: block; + } + @@ -285,46 +353,82 @@ > -
+
+

-

月次锁定管理

+ +
+ + ▶ 月次锁定管理 + -
- - +
+
+ + - - -
+ + +
- - + + -
+
-
- -

-    
+
+ +

+        
+
+
- +
@@ -426,52 +536,68 @@ let accounts = []; const ACCOUNT_GROUPS = [ + // 資産の部 { - label: "💰 現金・預金", + label: "現金・預金", codes: ["101", "102", "103", "104", "105"], }, { - label: "📦 流動資産", - codes: ["201", "202", "203", "204", "205", "210"], + label: "売上債権", + codes: ["201", "202"], }, { - label: "🏗️ 固定資産", - codes: [ - "301", - "401", - "402", - "403", - "404", - "405", - "406", - "407", - "408", - ], + label: "有価証券", + codes: ["301"], }, { - label: "💳 負債", - codes: [ - "501", - "502", - "503", - "504", - "505", - "506", - "507", - "508", - "509", - ], + label: "棚卸資産", + codes: ["210"], }, { - label: "🏢 純資産", - codes: ["601", "602"], + label: "他流動資産", + codes: ["203", "204", "205", "206", "207", "208"], }, { - label: "📈 収益", + label: "有形固定資産", + codes: ["401", "402", "403", "404", "405", "406", "407"], + }, + { + label: "無形固定資産", + codes: ["408"], + }, + { + label: "投資その他資産", + codes: ["410"], + }, + // 負債の部 + { + label: "仕入債務", + codes: ["501"], + }, + { + label: "その他流動負債", + codes: ["502", "503", "504", "505", "506", "507", "508"], + }, + { + label: "固定負債", + codes: ["509"], + }, + // 資本の部 + { + label: "資本金", + codes: ["601"], + }, + { + label: "利益剰余金", + codes: ["602"], + }, + // 収益・費用 + { + label: "収益", codes: ["701", "702", "703", "704", "705"], }, { - label: "📉 費用", + label: "費用", codes: [ "801", "802", @@ -483,6 +609,16 @@ "808", "809", "810", + "811", + "812", + "813", + "814", + "815", + "816", + "817", + "818", + "819", + "820", ], }, ]; @@ -491,9 +627,88 @@ let taxReceivedAccounts = []; let rowSeq = 0; // 行ID計数器 - // ----------------------------- + // ======================================== + // 账户搜索功能 + // ======================================== + function setupAccountSearch(inputElement, row) { + 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) { + dropdown.classList.remove("active"); + 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"); + }); + + // クリック以外の場所のドロップダウンを非表示 + document.addEventListener("click", (e) => { + if (!container.contains(e.target)) { + dropdown.classList.remove("active"); + } + }); + + // フォーカス時にドロップダウンを表示(既存値がある場合) + inputElement.addEventListener("focus", () => { + if (inputElement.value) { + dropdown.classList.add("active"); + } + }); + } + + // 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 || ""; + }; + + // ======================================== // 初期化 - // ----------------------------- + // ======================================== async function init() { console.log("🔧 init() 开始..."); const res = await fetch(`${API}/accounts`); @@ -566,25 +781,77 @@ // 検索用科目セレクタ // ----------------------------- function setupSearchAccountSelector() { - const select = document.getElementById("searchAccountId"); - select.innerHTML = ''; + // 新的可搜索输入框实现 + const inputElement = document.getElementById("searchAccountInput"); + if (!inputElement) { + console.log("searchAccountInput 不存在"); + return; + } + + const container = inputElement.closest(".account-search-container"); + if (!container) return; + + const dropdown = container.querySelector(".account-dropdown"); - ACCOUNT_GROUPS.forEach((group) => { - const optgroup = document.createElement("optgroup"); - optgroup.label = group.label; + inputElement.addEventListener("input", (e) => { + const searchText = e.target.value.toLowerCase().trim(); + if (!searchText) { + dropdown.classList.remove("active"); + inputElement.dataset.accountId = ""; + return; + } - const groupAccounts = accounts.filter((a) => - group.codes.includes(a.account_code), - ); - - groupAccounts.forEach((acc) => { - const option = document.createElement("option"); - option.value = acc.account_id; - option.textContent = `${acc.account_code} ${acc.account_name}`; - optgroup.appendChild(option); + const filtered = accounts.filter((acc) => { + const code = acc.account_code.toLowerCase(); + const name = acc.account_name.toLowerCase(); + return code.includes(searchText) || name.includes(searchText); }); - select.appendChild(optgroup); + 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"); + }); + + document.addEventListener("click", (e) => { + if (!container.contains(e.target)) { + dropdown.classList.remove("active"); + } + }); + + inputElement.addEventListener("focus", () => { + if (inputElement.value) { + inputElement.dispatchEvent(new Event("input")); + } }); } @@ -671,9 +938,15 @@ tr.innerHTML = ` - + 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") @@ -718,7 +995,7 @@ const recalc = () => handleTax(tr); const debitInput = tr.querySelector(".debit"); const creditInput = tr.querySelector(".credit"); - const accountSelect = tr.querySelector(".account"); + const accountInput = tr.querySelector(".account-search-input"); debitInput.addEventListener("input", recalc); creditInput.addEventListener("input", recalc); @@ -727,10 +1004,8 @@ debitInput.addEventListener("blur", updateTotals); creditInput.addEventListener("blur", updateTotals); - // 科目選択時に履歴を保存 - accountSelect.addEventListener("change", () => { - saveAccountToRecent(parseInt(accountSelect.value)); - }); + // 科目検索入力の処理 + setupAccountSearch(accountInput, tr); tr.querySelector(".taxType").addEventListener("change", recalc); tr.querySelector(".taxDirection").addEventListener("change", recalc); @@ -909,7 +1184,8 @@ rows.forEach((row) => { if (row.classList.contains("tax-row")) return; // 税行は無視 - const accountId = Number(row.querySelector(".account").value); + const accountInput = row.querySelector(".account-search-input"); + const accountId = Number(accountInput.dataset.accountId || 0); const debit = Number( row.querySelector(".debit").value.replace(/,/g, "") || 0, ); @@ -1015,18 +1291,28 @@ alert(`登録完了(ID=${result.journal_entry_id})`); - // フォームをクリア - document.getElementById("description").value = ""; - const tbody = document.querySelector("#linesTable tbody"); - tbody.innerHTML = ""; - rowSeq = 0; - addRow(); + // 「登録後に明細を保持する」チェックボックスを確認 + const keepDetails = document.getElementById( + "keepDetailsAfterSubmit", + ).checked; - // 借方合計・貸方合計を0にリセット - document.getElementById("totalDebit").textContent = "0"; - document.getElementById("totalCredit").textContent = "0"; - document.getElementById("balanceStatus").textContent = ""; - document.getElementById("balanceStatus").style.background = ""; + 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 { + // チェックボックスが選中の場合、何もクリアしない(日付、摘要、明細をすべて保持) + // ユーザーは日付だけを修正してから次の仕訳を登録する + } // 仕訳検索を自動実行して最新の登録状況を表示 searchJournals(); @@ -1447,13 +1733,19 @@ } } - // 検索結果を表に表示 + // 搜索函数(在选择搜索科目时被触发) + function performSearch() { + searchJournals(); + } + + // 检索结果を表に表示 async function searchJournals() { try { 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( @@ -1525,15 +1817,14 @@ if (key) { conditions.push(`摘要: ${key}`); } - const accountSelect = document.getElementById("searchAccountId"); - const accountName = - accountSelect.options[accountSelect.selectedIndex]?.text || ""; - if (accountName && accountName !== "-- すべて --") { + const accountInput = document.getElementById("searchAccountInput"); + const accountName = accountInput ? accountInput.value : ""; + if (accountName && accountName.trim() !== "") { conditions.push(`科目: ${accountName}`); } const sideSelect = document.getElementById("searchAccountSide"); const sideName = - sideSelect.options[sideSelect.selectedIndex]?.text || ""; + sideSelect ? sideSelect.options[sideSelect.selectedIndex]?.text : ""; if (sideName && sideName !== "-- すべて --") { conditions.push(`方向: ${sideName}`); } @@ -1696,9 +1987,11 @@ const tr = addRow(); // 科目を設定 - const accountSelect = tr.querySelector(".account"); - if (accountSelect) { - accountSelect.value = line.account_id; + 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; } // 借方・貸方金額を設定(カンマフォーマット付き) @@ -1835,9 +2128,18 @@ if ( conditions.accountId !== undefined && conditions.accountId !== null - ) - document.getElementById("searchAccountId").value = - conditions.accountId; + ) { + const accountInput = document.getElementById("searchAccountInput"); + if (accountInput && accounts && accounts.length > 0) { + const account = accounts.find( + (a) => a.account_id === conditions.accountId, + ); + if (account) { + accountInput.value = `${account.account_code} ${account.account_name}`; + accountInput.dataset.accountId = account.account_id; + } + } + } if ( conditions.accountSide !== undefined && conditions.accountSide !== null diff --git a/frontend/js/api.js b/frontend/js/api.js index 9b49c56..fe19be3 100644 --- a/frontend/js/api.js +++ b/frontend/js/api.js @@ -4,10 +4,20 @@ async function apiGet(path, params = {}) { const query = new URLSearchParams(params).toString(); const url = `${API_BASE}${path}?${query}`; + console.log(`📡 API GET: ${url}`); + const res = await fetch(url); + + console.log(`📊 Response status: ${res.status}`); + if (!res.ok) { const data = await res.json(); + console.error(`❌ API Error:`, data); throw new Error(data.detail || "APIエラー"); } - return res.json(); + + const jsonData = await res.json(); + console.log(`✅ API Response:`, jsonData); + + return jsonData; } diff --git a/frontend/js/opening_balance.js b/frontend/js/opening_balance.js index 02b980c..af5ca50 100644 --- a/frontend/js/opening_balance.js +++ b/frontend/js/opening_balance.js @@ -1,15 +1,22 @@ let accounts = []; let retainedEarnings = null; let isLocked = false; +let isDataLoaded = false; // ★ データ読込済みフラグ(保存時の検証用) -// 科目グループ定義(小計付き) +// 科目グループ定義(小計付き)- 試算表と同じ順序 const GROUPS = [ { key: "asset", label: "資産", subGroups: [ - { label: "現金・預金合計", codes: ["101", "102", "103", "104"] }, - { label: "売上債権合計", codes: ["201"] }, + { label: "現金・預金合計", codes: ["101", "102", "103", "104", "105"] }, + { label: "売上債権合計", codes: ["201", "202"] }, + { label: "有価証券合計", codes: ["301"] }, + { label: "棚卸資産合計", codes: ["210"] }, + { + label: "他流動資産合計", + codes: ["203", "204", "205", "206", "207", "208"], + }, { label: "流動資産合計", codes: [ @@ -17,14 +24,25 @@ const GROUPS = [ "102", "103", "104", + "105", "201", "202", "203", "204", "205", "206", + "207", + "208", + "210", + "301", ], }, + { + label: "有形固定資産合計", + codes: ["401", "402", "403", "404", "405", "406", "407"], + }, + { label: "無形固定資産合計", codes: ["408"] }, + { label: "投資その他の資産合計", codes: ["301", "410"] }, { label: "固定資産合計", codes: [ @@ -40,6 +58,35 @@ const GROUPS = [ "410", ], }, + { + label: "資産合計", + codes: [ + "101", + "102", + "103", + "104", + "105", + "201", + "202", + "203", + "204", + "205", + "206", + "207", + "208", + "210", + "301", + "401", + "402", + "403", + "404", + "405", + "406", + "407", + "408", + "410", + ], + }, ], }, { @@ -52,13 +99,17 @@ const GROUPS = [ codes: ["501", "502", "503", "504", "505", "506", "507", "508"], }, { label: "固定負債合計", codes: ["509"] }, + { + label: "負債合計", + codes: ["501", "502", "503", "504", "505", "506", "507", "508", "509"], + }, ], }, { key: "equity", label: "純資産", subGroups: [ - { label: "株主資本合計", codes: ["601", "602"] }, + { label: "資本金合計", codes: ["601"] }, { label: "純資産合計", codes: ["601", "602"] }, ], }, @@ -163,102 +214,67 @@ function updateSubtotalRows() { async function loadOpeningBalances() { const year = document.getElementById("fiscalYear").value; - const data = await apiGet("/opening-balances", { fiscal_year: year }); + console.log(`🔄 Loading opening balances for year: ${year}`); - accounts = data; - retainedEarnings = accounts.find( - (a) => a.account_name === "繰越利益剰余金" || a.account_name === "繰越利益", - ); + try { + const data = await apiGet("/opening-balances", { fiscal_year: year }); - const tbody = document.querySelector("#balanceTable tbody"); - tbody.innerHTML = ""; + console.log(`✓ Received data:`, data); + console.log(`✓ Total accounts: ${data.length}`); - GROUPS.forEach((group) => { - // 分组标题行 - const headerTr = document.createElement("tr"); - headerTr.innerHTML = ` - - ${group.label} - - `; - tbody.appendChild(headerTr); + accounts = data; + retainedEarnings = accounts.find( + (a) => + a.account_name === "繰越利益剰余金" || a.account_name === "繰越利益", + ); - const groupAccounts = accounts.filter((a) => a.account_type === group.key); + const tbody = document.querySelector("#balanceTable tbody"); + console.log(`📌 tbody element:`, tbody); - groupAccounts.forEach((a, idx) => { - const i = accounts.indexOf(a); + tbody.innerHTML = ""; + + // シンプルに全科目を表示(グループ分けは後で) + console.log(`🔄 Starting to render ${data.length} accounts...`); + + accounts.forEach((a, idx) => { const isRetained = a.account_name === "繰越利益剰余金" || a.account_name === "繰越利益"; + const tr = document.createElement("tr"); - tr.innerHTML = ` - ${a.account_code} - ${a.account_name} - - - - - - - `; - + ${a.account_code} + ${a.account_name} + + + + + + + `; tbody.appendChild(tr); - - // 小計行の追加 - group.subGroups.forEach((subGroup) => { - const isLastInSubGroup = - subGroup.codes.includes(a.account_code) && - !groupAccounts - .slice(idx + 1) - .some((nextAcc) => subGroup.codes.includes(nextAcc.account_code)); - - if (isLastInSubGroup) { - const subGroupTotal = accounts - .filter((acc) => subGroup.codes.includes(acc.account_code)) - .reduce( - (sum, acc) => ({ - debit: sum.debit + Number(acc.opening_debit || 0), - credit: sum.credit + Number(acc.opening_credit || 0), - }), - { debit: 0, credit: 0 }, - ); - - const totalTr = document.createElement("tr"); - totalTr.style.backgroundColor = "#d0d0d0"; - totalTr.style.fontWeight = "bold"; - totalTr.innerHTML = ` - - ${subGroup.label} - ${formatNumber(subGroupTotal.debit)} - ${formatNumber(subGroupTotal.credit)} - `; - tbody.appendChild(totalTr); - } - }); - }); - }); - - // 年度锁定チェック - fetch(`/opening-balances/lock-status?fiscal_year=${year}`) - .then((res) => res.json()) - .then((data) => { - isLocked = data.is_locked; - document.querySelector( - "button[onclick='saveOpeningBalances()']", - ).disabled = isLocked; - if (isLocked) { - alert("この会計年度の期首残高は確定されています。"); - } }); - recalcTotals(); + console.log(`✅ Table rendered!`, tbody.innerHTML.substring(0, 100)); + + isDataLoaded = true; // ★ データ読込済みフラグを設定 + recalcTotals(); + } catch (e) { + console.error("❌ Error loading opening balances:", e); + alert(`読込エラー: ${e.message}`); + } } async function saveOpeningBalances() { + // ★ 重要:データを読込まずに保存するのを防止 + if (!isDataLoaded) { + alert("先に「読込」ボタンで期首残高を読み込んでください。"); + return; + } + if (isLocked) { alert("この会計年度の期首残高は確定されているため、保存できません。"); return; @@ -279,6 +295,12 @@ async function saveOpeningBalances() { credit: Number(a.opening_credit || 0), })); + // ★ 重要:データが全て0の場合、保存を拒否 + if (balances.length === 0) { + alert("全科目の残高が0です。期首残高データを入力してください。"); + return; + } + try { await fetch(`/opening-balances`, { method: "POST", @@ -295,6 +317,8 @@ async function saveOpeningBalances() { }); alert("期首残高を保存しました"); + // メインメニューに移動 + location.href = "index.html"; } catch (e) { alert(e.message); } diff --git a/frontend/js/trial-balance.js b/frontend/js/trial-balance.js index 4d8bca7..39b9674 100644 --- a/frontend/js/trial-balance.js +++ b/frontend/js/trial-balance.js @@ -99,6 +99,11 @@ function initTrialBalance() { tr.classList.add("total-row"); } + // indent フラグに基づくクラス追加 + if (row.indent) { + tr.classList.add("indent-row"); + } + const ratio = row.ratio ?? "0.00"; tr.innerHTML = ` diff --git a/frontend/opening_balance.html b/frontend/opening_balance.html index 41f4b0d..443b719 100644 --- a/frontend/opening_balance.html +++ b/frontend/opening_balance.html @@ -39,10 +39,23 @@
- + diff --git a/frontend/trial-balance.html b/frontend/trial-balance.html index 18a0643..40cd39e 100644 --- a/frontend/trial-balance.html +++ b/frontend/trial-balance.html @@ -88,6 +88,9 @@ font-weight: bold; border-top: 2px solid #666; } + tr.indent-row td:nth-child(2) { + padding-left: 40px !important; + } tfoot td { font-weight: bold; background: #e0e0e0; diff --git a/list_accounts.py b/list_accounts.py new file mode 100644 index 0000000..379a537 --- /dev/null +++ b/list_accounts.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +import psycopg + +conn = psycopg.connect('host=localhost user=postgres password=postgres dbname=accounting') +cur = conn.cursor() +cur.execute('SELECT account_code, account_name FROM accounts WHERE is_active=true ORDER BY account_code') +for row in cur.fetchall(): + print(f'{row[0]:3s} {row[1]}') +cur.close() +conn.close() diff --git a/verify_bank_balance.py b/verify_bank_balance.py new file mode 100644 index 0000000..3fb833d --- /dev/null +++ b/verify_bank_balance.py @@ -0,0 +1,108 @@ +import psycopg2 + +conn = psycopg2.connect( + host="192.168.0.61", + port=55432, + database="njts_acct", + user="njts_app", + password="njts_app2025" +) + +cur = conn.cursor() + +print("=" * 160) +print("【普通預金の詳細確認】") +print("=" * 160) + +# 查询普通預金是否有遗漏删除的is_latest=false的记录 +cur.execute(""" + SELECT + COUNT(*) as total_count, + SUM(CASE WHEN je.is_deleted = false AND je.is_latest = true THEN 1 ELSE 0 END) as latest_count, + SUM(CASE WHEN je.is_deleted = false AND je.is_latest = false THEN 1 ELSE 0 END) as old_version_count, + SUM(CASE WHEN je.is_deleted = true THEN 1 ELSE 0 END) as deleted_count + FROM journal_lines jl + INNER JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE jl.account_id = 2 -- 普通預金 +""") + +total, latest, old_ver, deleted = cur.fetchone() +print(f"\n普通預金の仕訳カウント:") +print(f" 全体: {total}") +print(f" 最新版(is_latest=true): {latest}") +print(f" 古い版(is_deleted=false, is_latest=false): {old_ver}") +print(f" 削除済み(is_deleted=true): {deleted}") + +# 查询普通預金的各种状态的余额 +cur.execute(""" + SELECT + je.is_latest, + je.is_deleted, + COUNT(*) as cnt, + COALESCE(SUM(jl.debit), 0) as total_debit, + COALESCE(SUM(jl.credit), 0) as total_credit, + COALESCE(SUM(jl.debit), 0) - COALESCE(SUM(jl.credit), 0) as balance + FROM journal_lines jl + INNER JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE jl.account_id = 2 + GROUP BY je.is_latest, je.is_deleted + ORDER BY je.is_deleted, je.is_latest +""") + +print(f"\n普通預金の状態別残高:") +print() + +total_all = 0 +for is_latest, is_deleted, cnt, debit, credit, balance in cur.fetchall(): + status = f"is_latest={is_latest}, is_deleted={is_deleted}" + print(f" {status:35} | 件数={cnt:3} | 借={debit:>12,.0f} 貸={credit:>12,.0f} | 残高={balance:>12,.0f}円") + total_all += balance + +print(f"\n全て合計: {total_all:>15,.0f}円") + +# 查询只计算最新版本和非删除的 +cur.execute(""" + SELECT + COALESCE(SUM(COALESCE(jl.debit, 0) - COALESCE(jl.credit, 0)), 0) + FROM journal_lines jl + INNER JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE jl.account_id = 2 + AND je.is_deleted = false + AND je.is_latest = true +""") + +valid_balance = cur.fetchone()[0] +print(f"有効な残高(is_deleted=false, is_latest=true): {valid_balance:>15,.0f}円") + +# 比对 +print("\n" + "=" * 160) +print("【理想的な月別普通預金期末残高】") +print("=" * 160) + +cur.execute(""" + SELECT + EXTRACT(YEAR FROM je.entry_date)::INT as year, + EXTRACT(MONTH FROM je.entry_date)::INT as month, + CASE + WHEN EXTRACT(MONTH FROM je.entry_date) >= 6 + THEN EXTRACT(YEAR FROM je.entry_date) + ELSE EXTRACT(YEAR FROM je.entry_date) - 1 + END as fiscal_year, + COALESCE(SUM(COALESCE(jl.debit, 0) - COALESCE(jl.credit, 0)), 0) as balance + FROM journal_lines jl + INNER JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE jl.account_id = 2 + AND je.is_deleted = false + AND je.is_latest = true + AND EXTRACT(YEAR FROM je.entry_date) = 2025 + GROUP BY fiscal_year, year, month + ORDER BY month +""") + +results = cur.fetchall() +print() +for year, month, fy, balance in results: + print(f" {year}年{month:2}月:{balance:>15,.0f}円") + +cur.close() +conn.close() diff --git a/verify_opening_balances_table.py b/verify_opening_balances_table.py new file mode 100644 index 0000000..86e0f58 --- /dev/null +++ b/verify_opening_balances_table.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""opening_balances テーブルのデータ確認スクリプト""" +import psycopg + +print("="*60) +print("【opening_balances テーブルデータ確認】") +print("="*60) + +try: + conn = psycopg.connect('host=localhost user=postgres password=postgres dbname=accounting') + cur = conn.cursor() + + # 1. テーブル自体の存在確認 + cur.execute(""" + SELECT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_name = 'opening_balances' + ) + """) + table_exists = cur.fetchone()[0] + print(f"\n1️⃣ opening_balances テーブルの存在: {'✓ 存在' if table_exists else '❌ 存在しない'}") + + if not table_exists: + print(" → テーブルを作成する必要があります") + cur.close() + conn.close() + exit(1) + + # 2. 全行数 + cur.execute("SELECT COUNT(*) FROM opening_balances") + total_count = cur.fetchone()[0] + print(f"\n2️⃣ 全体の行数: {total_count}") + + if total_count == 0: + print(" ⚠️ opening_balances テーブルは空です") + else: + # 3. 年度別統計 + cur.execute(""" + SELECT fiscal_year, COUNT(*) as cnt + FROM opening_balances + GROUP BY fiscal_year + ORDER BY fiscal_year DESC + """) + print(f"\n3️⃣ 年度別件数:") + for row in cur.fetchall(): + print(f" - {row[0]}年度: {row[1]}件") + + # 4. 2025年度のデータ最初の5件 + print(f"\n4️⃣ 2025年度のサンプルデータ(最初の5件):") + cur.execute(""" + SELECT a.account_code, a.account_name, o.opening_debit, o.opening_credit + FROM opening_balances o + JOIN accounts a ON a.account_id = o.account_id + WHERE o.fiscal_year = 2025 + LIMIT 5 + """) + rows = cur.fetchall() + if rows: + for row in rows: + print(f" {row[0]:3s} {row[1]:20s} 借方:{row[2]:>12} 貸方:{row[3]:>12}") + else: + print(" → 2025年度のデータがありません") + + cur.close() + conn.close() + + print("\n" + "="*60) + +except Exception as e: + print(f"\n❌ エラー: {e}") + import traceback + traceback.print_exc()