This commit is contained in:
admin
2026-02-21 10:51:25 +09:00
parent 584530937b
commit 3b028b8fc0
25 changed files with 1852 additions and 244 deletions

View File

@@ -98,15 +98,29 @@ def _find_retained_earnings(cur) -> Optional[Tuple[int, str]]:
def save_opening_balances(req: OpeningBalanceRequest): def save_opening_balances(req: OpeningBalanceRequest):
with get_connection() as conn, conn.cursor() as cur: with get_connection() as conn, conn.cursor() as cur:
# 年度ロック # テーブルの存在確認(例外処理を避けるため)
cur.execute(""" table_exists = False
SELECT is_locked try:
FROM fiscal_year_locks cur.execute("""
WHERE fiscal_year = %s SELECT EXISTS (
""", (req.fiscal_year,)) SELECT 1 FROM information_schema.tables
row = cur.fetchone() WHERE table_name = 'fiscal_year_locks'
if row and row["is_locked"]: )
raise HTTPException(status_code=400, detail="この会計年度の期首残高は既に確定されています。") """)
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 科目取得(存在性/種別チェック用) # 全 BS 科目取得(存在性/種別チェック用)
cur.execute(""" 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()} 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のみ採用・繰越利益は無視後で自動作成
bs_items: List[OpeningBalanceItem] = [] bs_items: List[OpeningBalanceItem] = []
total_debit = Decimal("0") total_debit = Decimal("0")
@@ -145,6 +166,13 @@ def save_opening_balances(req: OpeningBalanceRequest):
total_credit += b.credit total_credit += b.credit
bs_items.append(b) bs_items.append(b)
# ★ 重要0/0の行だけで実質的にデータなし削除防止
if not bs_items:
raise HTTPException(
status_code=400,
detail="全科目の残高が0です。期首残高データを入力してください。"
)
# 繰越利益(剰余金)を自動計算 # 繰越利益(剰余金)を自動計算
diff = total_debit - total_credit # 借方合計 貸方合計 diff = total_debit - total_credit # 借方合計 貸方合計
re_account = _find_retained_earnings(cur) re_account = _find_retained_earnings(cur)

View File

@@ -9,14 +9,17 @@ def D(x) -> Decimal:
# 科目グループ定義(表示順序を制御)- PDFフォーマットに準拠 # 科目グループ定義(表示順序を制御)- PDFフォーマットに準拠
ACCOUNT_GROUPS = [ ACCOUNT_GROUPS = [
# 資産の部 # 資産の部
("現金・預金合計", ["101", "102", "103", "104"]), ("現金・預金合計", ["101", "102", "103", "104", "105"]),
("流動資産合計", ["101", "102", "103", "104", "201", "202", "203", "204", "205"]), ("売上債権合計", ["201", "202"]),
("有価証券合計", ["301"]), ("有価証券合計", ["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"]), ("有形固定資産合計", ["401", "402", "403", "404", "405", "406", "407"]),
("無形固定資産合計", ["408"]), ("無形固定資産合計", ["408"]),
("投資その他の資産合計", ["301", "410"]), ("投資その他の資産合計", ["301", "410"]),
("固定資産合計", ["301", "401", "402", "403", "404", "405", "406", "407", "408", "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"]),
("流動負債合計", ["501", "502", "503", "504", "505", "506", "507", "508"]), ("流動負債合計", ["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({ result_accounts.append({
"account_code": "", "account_code": "",
"account_name": " 繰越利益", "account_name": "繰越利益",
"account_type": "equity", "account_type": "equity",
"opening_balance": str(retained_earnings_opening), "opening_balance": str(retained_earnings_opening),
"debit": "0", "debit": "0",
@@ -183,7 +186,7 @@ def fetch_trial_balance(date_from: str, date_to: str):
# 当期純損益金額の行を追加 # 当期純損益金額の行を追加
result_accounts.append({ result_accounts.append({
"account_code": "", "account_code": "",
"account_name": " 当期純損益金額", "account_name": "当期純損益金額",
"account_type": "equity", "account_type": "equity",
"opening_balance": "0", "opening_balance": "0",
"debit": str(expense_total) if expense_total > 0 else "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({ result_accounts.append({
"account_code": "", "account_code": "",
"account_name": " 繰越利益剰余金合計", "account_name": "繰越利益剰余金合計",
"account_type": "equity", "account_type": "equity",
"opening_balance": str(retained_earnings_opening), "opening_balance": str(retained_earnings_opening),
"debit": str(expense_total) if expense_total > 0 else "0", "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 # その他利益剰余金合計現在は0
result_accounts.append({ result_accounts.append({
"account_code": "", "account_code": "",
"account_name": " その他利益剰余金合計", "account_name": "その他利益剰余金合計",
"account_type": "equity", "account_type": "equity",
"opening_balance": "0", "opening_balance": "0",
"debit": "0", "debit": "0",

View File

@@ -661,16 +661,26 @@ def update_journal_entry(journal_id: int, req: JournalEntryUpdateRequest):
@router.delete("/{journal_id}", summary="仕訳削除(ソフト削除)") @router.delete("/{journal_id}", summary="仕訳削除(ソフト削除)")
def delete_journal_entry(journal_id: int, req: JournalEntryDeleteRequest): 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 get_connection() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
# ① 削除対象の仕訳情報を取得
cur.execute(""" cur.execute("""
UPDATE journal_entries SELECT
SET is_deleted = true, journal_entry_id,
updated_at = CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Tokyo' original_entry_id,
WHERE journal_entry_id = %s is_latest,
AND is_deleted = false revision_count
RETURNING journal_entry_id FROM journal_entries
WHERE journal_entry_id = %s
AND is_deleted = false
""", (journal_id,)) """, (journal_id,))
row = cur.fetchone() row = cur.fetchone()
@@ -680,9 +690,42 @@ def delete_journal_entry(journal_id: int, req: JournalEntryDeleteRequest):
detail="仕訳が存在しないか、既に削除されています" 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() conn.commit()
return { return {
"status": "deleted", "status": "deleted",
"journal_entry_id": journal_id "journal_entry_id": journal_id,
"message": "仕訳を削除しました(完全な履歴は保持されます)"
} }

View File

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

View File

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

View File

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

52
check_account_types.py Normal file
View File

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

View File

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

38
check_db_simple.py Normal file
View File

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

125
check_deleted_entries.py Normal file
View File

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

139
check_opening_balance.py Normal file
View File

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

View File

@@ -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}")

120
check_total_balance.py Normal file
View File

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

14
expense_accounts.txt Normal file
View File

@@ -0,0 +1,14 @@
=== 试算表中的费用科目 ===
801 | 仕入高
802 | 外注費
803 | 旅費交通費
804 | 通信費
805 | 消耗品費
806 | 地代家賃
807 | 水道光熱費
808 | 支払手数料
809 | 雑費
810 | 支払報酬料
811 | 人件費
共 11 个费用科目

View File

@@ -96,6 +96,12 @@
style="margin-left: 10px; background: #28a745" style="margin-left: 10px; background: #28a745"
>試算表へ</a >試算表へ</a
> >
<a
class="btn"
href="opening_balance.html"
style="margin-left: 10px; background: #fd7e14"
>期首残高入力へ</a
>
</p> </p>
</section> </section>

View File

@@ -39,6 +39,67 @@
color: #090; color: #090;
margin-top: 8px; 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;
}
</style> </style>
</head> </head>
<body> <body>
@@ -84,6 +145,187 @@
const API = ""; const API = "";
let accounts = []; 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 = `<div style="padding: 8px 12px; color: #999;">該当する科目がありません</div>`;
} else {
filtered.slice(0, 20).forEach((acc) => {
const option = document.createElement("div");
option.className = "account-option";
option.innerHTML = `
<span class="account-option-strong">${acc.account_code}</span>
<span class="account-option-light">${acc.account_name}</span>
`;
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 += `<optgroup label="${group.label}">`;
accounts
.filter((a) => group.codes.includes(a.account_code))
.forEach((a) => {
html += `<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`;
});
html += `</optgroup>`;
});
// 未分組の科目を「その他」グループに追加
const groupedCodes = ACCOUNT_GROUPS.flatMap((g) => g.codes);
const others = accounts.filter(
(a) => !groupedCodes.includes(a.account_code),
);
if (others.length > 0) {
html += `<optgroup label="その他">`;
others.forEach((a) => {
html += `<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`;
});
html += `</optgroup>`;
}
return html;
}
// ------------------------------------ // ------------------------------------
// 初期化 // 初期化
// ------------------------------------ // ------------------------------------
@@ -122,14 +364,14 @@
tr.innerHTML = ` tr.innerHTML = `
<td> <td>
<select> <div class="account-search-container">
${accounts <input
.map( type="text"
(a) => class="account-search-input account"
`<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`, placeholder="科目を入力・検索"
) autocomplete="off" />
.join("")} <div class="account-dropdown"></div>
</select> </div>
</td> </td>
<td><input type="number" min="0"></td> <td><input type="number" min="0"></td>
<td><input type="number" min="0"></td> <td><input type="number" min="0"></td>
@@ -138,10 +380,19 @@
tbody.appendChild(tr); tbody.appendChild(tr);
// Setup account search for this row
const accountInput = tr.querySelector(".account-search-input");
setupAccountSearch(accountInput, tr);
if (line) { if (line) {
tr.querySelector("select").value = line.account_id; // Find the account and populate the input
tr.querySelectorAll("input")[0].value = line.debit || ""; const account = accounts.find(a => a.account_id === line.account_id);
tr.querySelectorAll("input")[1].value = line.credit || ""; 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; c = 0;
document.querySelectorAll("#linesTable tbody tr").forEach((tr) => { document.querySelectorAll("#linesTable tbody tr").forEach((tr) => {
const accountId = Number(tr.querySelector("select").value); const accountInput = tr.querySelector(".account-search-input");
const debit = Number(tr.querySelectorAll("input")[0].value || 0); const accountId = Number(accountInput.dataset.accountId || 0);
const credit = Number(tr.querySelectorAll("input")[1].value || 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; if (debit === 0 && credit === 0) return;
lines.push({ account_id: accountId, debit, credit }); lines.push({ account_id: accountId, debit, credit });
d += debit; d += debit;

View File

@@ -184,6 +184,74 @@
display: table-header-group; 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;
}
</style> </style>
</head> </head>
<body> <body>
@@ -285,46 +353,82 @@
></div> ></div>
</div> </div>
<div style="margin-top: 10px; display: flex; gap: 8px"> <div style="margin-top: 10px; display: flex; gap: 8px; align-items: center">
<button onclick="addRow()"> 行追加</button> <button onclick="addRow()"> 行追加</button>
<button onclick="submitJournal()">登録</button> <button onclick="submitJournal()">登録</button>
<label
style="
display: flex;
align-items: center;
gap: 6px;
margin-left: 20px;
cursor: pointer;
"
>
<input type="checkbox" id="keepDetailsAfterSubmit" />
<span style="font-size: 14px">登録後に明細を保持する</span>
</label>
</div> </div>
<hr style="margin: 40px 0; border: none; border-top: 2px solid #ddd" /> <hr style="margin: 40px 0; border: none; border-top: 2px solid #ddd" />
<h3>月次锁定管理</h3> <!-- 月次锁定管理(折叠式) -->
<details style="margin: 16px 0">
<summary
style="
cursor: pointer;
font-size: 18px;
font-weight: bold;
padding: 12px;
background: #f5f5f5;
border-radius: 4px;
user-select: none;
"
>
▶ 月次锁定管理
</summary>
<div style="margin-bottom: 8px"> <div
<label>年度:</label> style="
<input padding: 16px;
type="number" background: #fafafa;
id="lockYear" border-radius: 4px;
value="2025" margin-top: 8px;
min="1900" "
max="2999" >
style="width: 80px" <div style="margin-bottom: 8px">
/> <label>年度:</label>
<input
type="number"
id="lockYear"
value="2025"
min="1900"
max="2999"
style="width: 80px"
/>
<label>月份:</label> <label>月份:</label>
<input <input
type="number" type="number"
id="lockMonth" id="lockMonth"
min="1" min="1"
max="12" max="12"
value="6" value="6"
style="width: 60px" style="width: 60px"
/> />
</div> </div>
<button onclick="lockMonth()">🔒 锁定</button> <button onclick="lockMonth()">🔒 锁定</button>
<button onclick="unlockMonth()">🔓 解锁</button> <button onclick="unlockMonth()">🔓 解锁</button>
<div id="lockResult" style="margin-top: 10px; color: #333"></div> <div id="lockResult" style="margin-top: 10px; color: #333"></div>
<div style="margin-top: 12px"> <div style="margin-top: 12px">
<button onclick="refreshMonthLocks()">🔄 刷新锁定列表</button> <button onclick="refreshMonthLocks()">🔄 刷新锁定列表</button>
<pre id="lockList" style="background: #f7f7f7; padding: 8px"></pre> <pre id="lockList" style="background: #f7f7f7; padding: 8px"></pre>
</div> </div>
</div>
</details>
<div <div
id="monthLockBanner" id="monthLockBanner"
@@ -370,9 +474,15 @@
<div class="search-form-row"> <div class="search-form-row">
<label>科目</label> <label>科目</label>
<select id="searchAccountId"> <div class="account-search-container">
<option value="">-- すべて --</option> <input
</select> type="text"
id="searchAccountInput"
class="account-search-input"
placeholder="科目を入力・検索"
autocomplete="off" />
<div class="account-dropdown"></div>
</div>
</div> </div>
<div class="search-form-row" style="grid-column: 1 / -1"> <div class="search-form-row" style="grid-column: 1 / -1">
@@ -426,52 +536,68 @@
let accounts = []; let accounts = [];
const ACCOUNT_GROUPS = [ const ACCOUNT_GROUPS = [
// 資産の部
{ {
label: "💰 現金・預金", label: "現金・預金",
codes: ["101", "102", "103", "104", "105"], codes: ["101", "102", "103", "104", "105"],
}, },
{ {
label: "📦 流動資産", label: "売上債権",
codes: ["201", "202", "203", "204", "205", "210"], codes: ["201", "202"],
}, },
{ {
label: "🏗️ 固定資産", label: "有価証券",
codes: [ codes: ["301"],
"301",
"401",
"402",
"403",
"404",
"405",
"406",
"407",
"408",
],
}, },
{ {
label: "💳 負債", label: "棚卸資産",
codes: [ codes: ["210"],
"501",
"502",
"503",
"504",
"505",
"506",
"507",
"508",
"509",
],
}, },
{ {
label: "🏢 純資産", label: "他流動資産",
codes: ["601", "602"], 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"], codes: ["701", "702", "703", "704", "705"],
}, },
{ {
label: "📉 費用", label: "費用",
codes: [ codes: [
"801", "801",
"802", "802",
@@ -483,6 +609,16 @@
"808", "808",
"809", "809",
"810", "810",
"811",
"812",
"813",
"814",
"815",
"816",
"817",
"818",
"819",
"820",
], ],
}, },
]; ];
@@ -491,9 +627,88 @@
let taxReceivedAccounts = []; let taxReceivedAccounts = [];
let rowSeq = 0; // 行ID計数器 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 = `<div style="padding: 8px 12px; color: #999;">該当する科目がありません</div>`;
} else {
filtered.slice(0, 20).forEach((acc) => {
const option = document.createElement("div");
option.className = "account-option";
option.innerHTML = `
<span class="account-option-strong">${acc.account_code}</span>
<span class="account-option-light">${acc.account_name}</span>
`;
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() { async function init() {
console.log("🔧 init() 开始..."); console.log("🔧 init() 开始...");
const res = await fetch(`${API}/accounts`); const res = await fetch(`${API}/accounts`);
@@ -566,25 +781,77 @@
// 検索用科目セレクタ // 検索用科目セレクタ
// ----------------------------- // -----------------------------
function setupSearchAccountSelector() { function setupSearchAccountSelector() {
const select = document.getElementById("searchAccountId"); // 新的可搜索输入框实现
select.innerHTML = '<option value="">-- すべて --</option>'; const inputElement = document.getElementById("searchAccountInput");
if (!inputElement) {
console.log("searchAccountInput 不存在");
return;
}
ACCOUNT_GROUPS.forEach((group) => { const container = inputElement.closest(".account-search-container");
const optgroup = document.createElement("optgroup"); if (!container) return;
optgroup.label = group.label;
const groupAccounts = accounts.filter((a) => const dropdown = container.querySelector(".account-dropdown");
group.codes.includes(a.account_code),
);
groupAccounts.forEach((acc) => { inputElement.addEventListener("input", (e) => {
const option = document.createElement("option"); const searchText = e.target.value.toLowerCase().trim();
option.value = acc.account_id; if (!searchText) {
option.textContent = `${acc.account_code} ${acc.account_name}`; dropdown.classList.remove("active");
optgroup.appendChild(option); inputElement.dataset.accountId = "";
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);
}); });
select.appendChild(optgroup); dropdown.innerHTML = "";
const all = document.createElement("div");
all.className = "account-option";
all.innerHTML = `<span class="account-option-strong">-- すべて --</span>`;
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 = `<span class="account-option-strong">${acc.account_code}</span><span class="account-option-light">${acc.account_name}</span>`;
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 = ` tr.innerHTML = `
<td> <td>
<select class="account" ${isTaxRow ? "disabled" : ""}> <div class="account-search-container">
${buildAccountOptions()} <input
</select> type="text"
class="account-search-input account"
placeholder="科目を入力・検索"
autocomplete="off"
${isTaxRow ? "readonly" : ""} />
<div class="account-dropdown"></div>
</div>
</td> </td>
<td><input type="text" class="debit right" inputmode="numeric" style="ime-mode: inactive;" ${ <td><input type="text" class="debit right" inputmode="numeric" style="ime-mode: inactive;" ${
isTaxRow ? "readonly" : "" isTaxRow ? "readonly" : ""
@@ -705,8 +978,12 @@
// 税行の場合,填值 // 税行の場合,填值
if (isTaxRow && taxInfo) { if (isTaxRow && taxInfo) {
const acc = tr.querySelector(".account"); const accountInput = tr.querySelector(".account-search-input");
acc.value = taxInfo.accountId || ""; 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") if (taxInfo.side === "debit")
tr.querySelector(".debit").value = taxInfo.amount; tr.querySelector(".debit").value = taxInfo.amount;
if (taxInfo.side === "credit") if (taxInfo.side === "credit")
@@ -718,7 +995,7 @@
const recalc = () => handleTax(tr); const recalc = () => handleTax(tr);
const debitInput = tr.querySelector(".debit"); const debitInput = tr.querySelector(".debit");
const creditInput = tr.querySelector(".credit"); const creditInput = tr.querySelector(".credit");
const accountSelect = tr.querySelector(".account"); const accountInput = tr.querySelector(".account-search-input");
debitInput.addEventListener("input", recalc); debitInput.addEventListener("input", recalc);
creditInput.addEventListener("input", recalc); creditInput.addEventListener("input", recalc);
@@ -727,10 +1004,8 @@
debitInput.addEventListener("blur", updateTotals); debitInput.addEventListener("blur", updateTotals);
creditInput.addEventListener("blur", updateTotals); creditInput.addEventListener("blur", updateTotals);
// 科目選択時に履歴を保存 // 科目検索入力の処理
accountSelect.addEventListener("change", () => { setupAccountSearch(accountInput, tr);
saveAccountToRecent(parseInt(accountSelect.value));
});
tr.querySelector(".taxType").addEventListener("change", recalc); tr.querySelector(".taxType").addEventListener("change", recalc);
tr.querySelector(".taxDirection").addEventListener("change", recalc); tr.querySelector(".taxDirection").addEventListener("change", recalc);
@@ -909,7 +1184,8 @@
rows.forEach((row) => { rows.forEach((row) => {
if (row.classList.contains("tax-row")) return; // 税行は無視 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( const debit = Number(
row.querySelector(".debit").value.replace(/,/g, "") || 0, row.querySelector(".debit").value.replace(/,/g, "") || 0,
); );
@@ -1015,18 +1291,28 @@
alert(`登録完了ID=${result.journal_entry_id}`); alert(`登録完了ID=${result.journal_entry_id}`);
// フォームをクリア // 「登録後に明細を保持する」チェックボックスを確認
document.getElementById("description").value = ""; const keepDetails = document.getElementById(
const tbody = document.querySelector("#linesTable tbody"); "keepDetailsAfterSubmit",
tbody.innerHTML = ""; ).checked;
rowSeq = 0;
addRow();
// 借方合計・貸方合計を0にリセット if (!keepDetails) {
document.getElementById("totalDebit").textContent = "0"; // チェックボックスが未選中の場合、フォームをクリア(従来の挙動)
document.getElementById("totalCredit").textContent = "0"; document.getElementById("description").value = "";
document.getElementById("balanceStatus").textContent = ""; const tbody = document.querySelector("#linesTable tbody");
document.getElementById("balanceStatus").style.background = ""; 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(); searchJournals();
@@ -1447,13 +1733,19 @@
} }
} }
// 検索結果を表に表示 // 搜索函数(在选择搜索科目时被触发)
function performSearch() {
searchJournals();
}
// 检索结果を表に表示
async function searchJournals() { async function searchJournals() {
try { try {
const from = document.getElementById("searchFromDate").value; const from = document.getElementById("searchFromDate").value;
const to = document.getElementById("searchToDate").value; const to = document.getElementById("searchToDate").value;
const key = document.getElementById("searchKeyword").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 = const accountSide =
document.getElementById("searchAccountSide").value; document.getElementById("searchAccountSide").value;
const includeHistory = document.getElementById( const includeHistory = document.getElementById(
@@ -1525,15 +1817,14 @@
if (key) { if (key) {
conditions.push(`摘要: ${key}`); conditions.push(`摘要: ${key}`);
} }
const accountSelect = document.getElementById("searchAccountId"); const accountInput = document.getElementById("searchAccountInput");
const accountName = const accountName = accountInput ? accountInput.value : "";
accountSelect.options[accountSelect.selectedIndex]?.text || ""; if (accountName && accountName.trim() !== "") {
if (accountName && accountName !== "-- すべて --") {
conditions.push(`科目: ${accountName}`); conditions.push(`科目: ${accountName}`);
} }
const sideSelect = document.getElementById("searchAccountSide"); const sideSelect = document.getElementById("searchAccountSide");
const sideName = const sideName =
sideSelect.options[sideSelect.selectedIndex]?.text || ""; sideSelect ? sideSelect.options[sideSelect.selectedIndex]?.text : "";
if (sideName && sideName !== "-- すべて --") { if (sideName && sideName !== "-- すべて --") {
conditions.push(`方向: ${sideName}`); conditions.push(`方向: ${sideName}`);
} }
@@ -1696,9 +1987,11 @@
const tr = addRow(); const tr = addRow();
// 科目を設定 // 科目を設定
const accountSelect = tr.querySelector(".account"); const accountInput = tr.querySelector(".account-search-input");
if (accountSelect) { const account = accounts.find(a => a.account_id === line.account_id);
accountSelect.value = 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 ( if (
conditions.accountId !== undefined && conditions.accountId !== undefined &&
conditions.accountId !== null conditions.accountId !== null
) ) {
document.getElementById("searchAccountId").value = const accountInput = document.getElementById("searchAccountInput");
conditions.accountId; 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 ( if (
conditions.accountSide !== undefined && conditions.accountSide !== undefined &&
conditions.accountSide !== null conditions.accountSide !== null

View File

@@ -4,10 +4,20 @@ async function apiGet(path, params = {}) {
const query = new URLSearchParams(params).toString(); const query = new URLSearchParams(params).toString();
const url = `${API_BASE}${path}?${query}`; const url = `${API_BASE}${path}?${query}`;
console.log(`📡 API GET: ${url}`);
const res = await fetch(url); const res = await fetch(url);
console.log(`📊 Response status: ${res.status}`);
if (!res.ok) { if (!res.ok) {
const data = await res.json(); const data = await res.json();
console.error(`❌ API Error:`, data);
throw new Error(data.detail || "APIエラー"); throw new Error(data.detail || "APIエラー");
} }
return res.json();
const jsonData = await res.json();
console.log(`✅ API Response:`, jsonData);
return jsonData;
} }

View File

@@ -1,15 +1,22 @@
let accounts = []; let accounts = [];
let retainedEarnings = null; let retainedEarnings = null;
let isLocked = false; let isLocked = false;
let isDataLoaded = false; // ★ データ読込済みフラグ(保存時の検証用)
// 科目グループ定義(小計付き) // 科目グループ定義(小計付き)- 試算表と同じ順序
const GROUPS = [ const GROUPS = [
{ {
key: "asset", key: "asset",
label: "資産", label: "資産",
subGroups: [ subGroups: [
{ label: "現金・預金合計", codes: ["101", "102", "103", "104"] }, { label: "現金・預金合計", codes: ["101", "102", "103", "104", "105"] },
{ label: "売上債権合計", codes: ["201"] }, { label: "売上債権合計", codes: ["201", "202"] },
{ label: "有価証券合計", codes: ["301"] },
{ label: "棚卸資産合計", codes: ["210"] },
{
label: "他流動資産合計",
codes: ["203", "204", "205", "206", "207", "208"],
},
{ {
label: "流動資産合計", label: "流動資産合計",
codes: [ codes: [
@@ -17,14 +24,25 @@ const GROUPS = [
"102", "102",
"103", "103",
"104", "104",
"105",
"201", "201",
"202", "202",
"203", "203",
"204", "204",
"205", "205",
"206", "206",
"207",
"208",
"210",
"301",
], ],
}, },
{
label: "有形固定資産合計",
codes: ["401", "402", "403", "404", "405", "406", "407"],
},
{ label: "無形固定資産合計", codes: ["408"] },
{ label: "投資その他の資産合計", codes: ["301", "410"] },
{ {
label: "固定資産合計", label: "固定資産合計",
codes: [ codes: [
@@ -40,6 +58,35 @@ const GROUPS = [
"410", "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"], codes: ["501", "502", "503", "504", "505", "506", "507", "508"],
}, },
{ label: "固定負債合計", codes: ["509"] }, { label: "固定負債合計", codes: ["509"] },
{
label: "負債合計",
codes: ["501", "502", "503", "504", "505", "506", "507", "508", "509"],
},
], ],
}, },
{ {
key: "equity", key: "equity",
label: "純資産", label: "純資産",
subGroups: [ subGroups: [
{ label: "株主資本合計", codes: ["601", "602"] }, { label: "資本合計", codes: ["601"] },
{ label: "純資産合計", codes: ["601", "602"] }, { label: "純資産合計", codes: ["601", "602"] },
], ],
}, },
@@ -163,102 +214,67 @@ function updateSubtotalRows() {
async function loadOpeningBalances() { async function loadOpeningBalances() {
const year = document.getElementById("fiscalYear").value; 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; try {
retainedEarnings = accounts.find( const data = await apiGet("/opening-balances", { fiscal_year: year });
(a) => a.account_name === "繰越利益剰余金" || a.account_name === "繰越利益",
);
const tbody = document.querySelector("#balanceTable tbody"); console.log(`✓ Received data:`, data);
tbody.innerHTML = ""; console.log(`✓ Total accounts: ${data.length}`);
GROUPS.forEach((group) => { accounts = data;
// 分组标题行 retainedEarnings = accounts.find(
const headerTr = document.createElement("tr"); (a) =>
headerTr.innerHTML = ` a.account_name === "繰越利益剰余金" || a.account_name === "繰越利益",
<td colspan="4" style="background:#ddd; font-weight:bold;"> );
${group.label}
</td>
`;
tbody.appendChild(headerTr);
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) => { tbody.innerHTML = "";
const i = accounts.indexOf(a);
// シンプルに全科目を表示(グループ分けは後で)
console.log(`🔄 Starting to render ${data.length} accounts...`);
accounts.forEach((a, idx) => {
const isRetained = const isRetained =
a.account_name === "繰越利益剰余金" || a.account_name === "繰越利益"; a.account_name === "繰越利益剰余金" || a.account_name === "繰越利益";
const tr = document.createElement("tr"); const tr = document.createElement("tr");
tr.innerHTML = ` tr.innerHTML = `
<td>${a.account_code}</td> <td>${a.account_code}</td>
<td>${a.account_name}</td> <td>${a.account_name}</td>
<td> <td>
<input type="text" value="${formatNumber(a.opening_debit)}" <input type="text" value="${formatNumber(a.opening_debit)}"
${isRetained ? "readonly" : ""} ${isRetained ? "readonly" : ""}
onchange="accounts[${i}].opening_debit=parseNumber(this.value); this.value=formatNumber(accounts[${i}].opening_debit); recalcTotals();"> onchange="accounts[${idx}].opening_debit=parseNumber(this.value); this.value=formatNumber(accounts[${idx}].opening_debit); recalcTotals();">
</td> </td>
<td> <td>
<input type="text" value="${formatNumber(a.opening_credit)}" <input type="text" value="${formatNumber(a.opening_credit)}"
${isRetained ? "readonly" : ""} ${isRetained ? "readonly" : ""}
onchange="accounts[${i}].opening_credit=parseNumber(this.value); this.value=formatNumber(accounts[${i}].opening_credit); recalcTotals();"> onchange="accounts[${idx}].opening_credit=parseNumber(this.value); this.value=formatNumber(accounts[${idx}].opening_credit); recalcTotals();">
</td> </td>
`; `;
tbody.appendChild(tr); 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 = `
<td></td>
<td>${subGroup.label}</td>
<td>${formatNumber(subGroupTotal.debit)}</td>
<td>${formatNumber(subGroupTotal.credit)}</td>
`;
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() { async function saveOpeningBalances() {
// ★ 重要:データを読込まずに保存するのを防止
if (!isDataLoaded) {
alert("先に「読込」ボタンで期首残高を読み込んでください。");
return;
}
if (isLocked) { if (isLocked) {
alert("この会計年度の期首残高は確定されているため、保存できません。"); alert("この会計年度の期首残高は確定されているため、保存できません。");
return; return;
@@ -279,6 +295,12 @@ async function saveOpeningBalances() {
credit: Number(a.opening_credit || 0), credit: Number(a.opening_credit || 0),
})); }));
// ★ 重要データが全て0の場合、保存を拒否
if (balances.length === 0) {
alert("全科目の残高が0です。期首残高データを入力してください。");
return;
}
try { try {
await fetch(`/opening-balances`, { await fetch(`/opening-balances`, {
method: "POST", method: "POST",
@@ -295,6 +317,8 @@ async function saveOpeningBalances() {
}); });
alert("期首残高を保存しました"); alert("期首残高を保存しました");
// メインメニューに移動
location.href = "index.html";
} catch (e) { } catch (e) {
alert(e.message); alert(e.message);
} }

View File

@@ -99,6 +99,11 @@ function initTrialBalance() {
tr.classList.add("total-row"); tr.classList.add("total-row");
} }
// indent フラグに基づくクラス追加
if (row.indent) {
tr.classList.add("indent-row");
}
const ratio = row.ratio ?? "0.00"; const ratio = row.ratio ?? "0.00";
tr.innerHTML = ` tr.innerHTML = `

View File

@@ -39,10 +39,23 @@
<br /> <br />
<button onclick="saveOpeningBalances()">保存</button>
<button <button
onclick="location.href = 'trial-balance.html'" onclick="saveOpeningBalances()"
class="back-button" style="
margin: 0 10px 0 0;
padding: 8px 16px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
"
>
保存
</button>
<button
onclick="location.href = 'index.html'"
style=" style="
margin: 0 0 20px 0; margin: 0 0 20px 0;
padding: 8px 16px; padding: 8px 16px;
@@ -54,7 +67,7 @@
font-size: 14px; font-size: 14px;
" "
> >
試算表に戻る メインメニューに戻る
</button> </button>
<script src="js/api.js"></script> <script src="js/api.js"></script>

View File

@@ -88,6 +88,9 @@
font-weight: bold; font-weight: bold;
border-top: 2px solid #666; border-top: 2px solid #666;
} }
tr.indent-row td:nth-child(2) {
padding-left: 40px !important;
}
tfoot td { tfoot td {
font-weight: bold; font-weight: bold;
background: #e0e0e0; background: #e0e0e0;

10
list_accounts.py Normal file
View File

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

108
verify_bank_balance.py Normal file
View File

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

View File

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