修正
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "仕訳を削除しました(完全な履歴は保持されます)"
|
||||
}
|
||||
|
||||
39
backend/create_fiscal_year_locks.py
Normal file
39
backend/create_fiscal_year_locks.py
Normal 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)
|
||||
61
backend/create_fiscal_year_locks_v2.py
Normal file
61
backend/create_fiscal_year_locks_v2.py
Normal 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
|
||||
16
backend/sql/create_fiscal_year_locks.sql
Normal file
16
backend/sql/create_fiscal_year_locks.sql
Normal 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
52
check_account_types.py
Normal 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()
|
||||
86
check_asset_composition.py
Normal file
86
check_asset_composition.py
Normal 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
38
check_db_simple.py
Normal 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
125
check_deleted_entries.py
Normal 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
139
check_opening_balance.py
Normal 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()
|
||||
39
check_opening_balances_data.py
Normal file
39
check_opening_balances_data.py
Normal 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
120
check_total_balance.py
Normal 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
14
expense_accounts.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
=== 试算表中的费用科目 ===
|
||||
801 | 仕入高
|
||||
802 | 外注費
|
||||
803 | 旅費交通費
|
||||
804 | 通信費
|
||||
805 | 消耗品費
|
||||
806 | 地代家賃
|
||||
807 | 水道光熱費
|
||||
808 | 支払手数料
|
||||
809 | 雑費
|
||||
810 | 支払報酬料
|
||||
811 | 人件費
|
||||
|
||||
共 11 个费用科目
|
||||
@@ -96,6 +96,12 @@
|
||||
style="margin-left: 10px; background: #28a745"
|
||||
>試算表へ</a
|
||||
>
|
||||
<a
|
||||
class="btn"
|
||||
href="opening_balance.html"
|
||||
style="margin-left: 10px; background: #fd7e14"
|
||||
>期首残高入力へ</a
|
||||
>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -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 = `<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 = `
|
||||
<td>
|
||||
<select>
|
||||
${accounts
|
||||
.map(
|
||||
(a) =>
|
||||
`<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`,
|
||||
)
|
||||
.join("")}
|
||||
</select>
|
||||
<div class="account-search-container">
|
||||
<input
|
||||
type="text"
|
||||
class="account-search-input account"
|
||||
placeholder="科目を入力・検索"
|
||||
autocomplete="off" />
|
||||
<div class="account-dropdown"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td><input type="number" min="0"></td>
|
||||
<td><input type="number" min="0"></td>
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -285,46 +353,82 @@
|
||||
></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="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>
|
||||
|
||||
<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">
|
||||
<label>年度:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="lockYear"
|
||||
value="2025"
|
||||
min="1900"
|
||||
max="2999"
|
||||
style="width: 80px"
|
||||
/>
|
||||
<div
|
||||
style="
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
margin-top: 8px;
|
||||
"
|
||||
>
|
||||
<div style="margin-bottom: 8px">
|
||||
<label>年度:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="lockYear"
|
||||
value="2025"
|
||||
min="1900"
|
||||
max="2999"
|
||||
style="width: 80px"
|
||||
/>
|
||||
|
||||
<label>月份:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="lockMonth"
|
||||
min="1"
|
||||
max="12"
|
||||
value="6"
|
||||
style="width: 60px"
|
||||
/>
|
||||
</div>
|
||||
<label>月份:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="lockMonth"
|
||||
min="1"
|
||||
max="12"
|
||||
value="6"
|
||||
style="width: 60px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button onclick="lockMonth()">🔒 锁定</button>
|
||||
<button onclick="unlockMonth()">🔓 解锁</button>
|
||||
<button onclick="lockMonth()">🔒 锁定</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">
|
||||
<button onclick="refreshMonthLocks()">🔄 刷新锁定列表</button>
|
||||
<pre id="lockList" style="background: #f7f7f7; padding: 8px"></pre>
|
||||
</div>
|
||||
<div style="margin-top: 12px">
|
||||
<button onclick="refreshMonthLocks()">🔄 刷新锁定列表</button>
|
||||
<pre id="lockList" style="background: #f7f7f7; padding: 8px"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div
|
||||
id="monthLockBanner"
|
||||
@@ -370,9 +474,15 @@
|
||||
|
||||
<div class="search-form-row">
|
||||
<label>科目</label>
|
||||
<select id="searchAccountId">
|
||||
<option value="">-- すべて --</option>
|
||||
</select>
|
||||
<div class="account-search-container">
|
||||
<input
|
||||
type="text"
|
||||
id="searchAccountInput"
|
||||
class="account-search-input"
|
||||
placeholder="科目を入力・検索"
|
||||
autocomplete="off" />
|
||||
<div class="account-dropdown"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-form-row" style="grid-column: 1 / -1">
|
||||
@@ -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 = `<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() {
|
||||
console.log("🔧 init() 开始...");
|
||||
const res = await fetch(`${API}/accounts`);
|
||||
@@ -566,25 +781,77 @@
|
||||
// 検索用科目セレクタ
|
||||
// -----------------------------
|
||||
function setupSearchAccountSelector() {
|
||||
const select = document.getElementById("searchAccountId");
|
||||
select.innerHTML = '<option value="">-- すべて --</option>';
|
||||
// 新的可搜索输入框实现
|
||||
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 = `<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 = `
|
||||
<td>
|
||||
<select class="account" ${isTaxRow ? "disabled" : ""}>
|
||||
${buildAccountOptions()}
|
||||
</select>
|
||||
<div class="account-search-container">
|
||||
<input
|
||||
type="text"
|
||||
class="account-search-input account"
|
||||
placeholder="科目を入力・検索"
|
||||
autocomplete="off"
|
||||
${isTaxRow ? "readonly" : ""} />
|
||||
<div class="account-dropdown"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td><input type="text" class="debit right" inputmode="numeric" style="ime-mode: inactive;" ${
|
||||
isTaxRow ? "readonly" : ""
|
||||
@@ -705,8 +978,12 @@
|
||||
|
||||
// 税行の場合,填值
|
||||
if (isTaxRow && taxInfo) {
|
||||
const acc = tr.querySelector(".account");
|
||||
acc.value = taxInfo.accountId || "";
|
||||
const accountInput = tr.querySelector(".account-search-input");
|
||||
const account = accounts.find(a => a.account_id === taxInfo.accountId);
|
||||
if (account) {
|
||||
accountInput.value = `${account.account_code} ${account.account_name}`;
|
||||
accountInput.dataset.accountId = account.account_id;
|
||||
}
|
||||
if (taxInfo.side === "debit")
|
||||
tr.querySelector(".debit").value = taxInfo.amount;
|
||||
if (taxInfo.side === "credit")
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = `
|
||||
<td colspan="4" style="background:#ddd; font-weight:bold;">
|
||||
${group.label}
|
||||
</td>
|
||||
`;
|
||||
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 = `
|
||||
<td>${a.account_code}</td>
|
||||
<td>${a.account_name}</td>
|
||||
<td>
|
||||
<input type="text" value="${formatNumber(a.opening_debit)}"
|
||||
${isRetained ? "readonly" : ""}
|
||||
onchange="accounts[${i}].opening_debit=parseNumber(this.value); this.value=formatNumber(accounts[${i}].opening_debit); recalcTotals();">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" value="${formatNumber(a.opening_credit)}"
|
||||
${isRetained ? "readonly" : ""}
|
||||
onchange="accounts[${i}].opening_credit=parseNumber(this.value); this.value=formatNumber(accounts[${i}].opening_credit); recalcTotals();">
|
||||
</td>
|
||||
`;
|
||||
|
||||
<td>${a.account_code}</td>
|
||||
<td>${a.account_name}</td>
|
||||
<td>
|
||||
<input type="text" value="${formatNumber(a.opening_debit)}"
|
||||
${isRetained ? "readonly" : ""}
|
||||
onchange="accounts[${idx}].opening_debit=parseNumber(this.value); this.value=formatNumber(accounts[${idx}].opening_debit); recalcTotals();">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" value="${formatNumber(a.opening_credit)}"
|
||||
${isRetained ? "readonly" : ""}
|
||||
onchange="accounts[${idx}].opening_credit=parseNumber(this.value); this.value=formatNumber(accounts[${idx}].opening_credit); recalcTotals();">
|
||||
</td>
|
||||
`;
|
||||
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() {
|
||||
// ★ 重要:データを読込まずに保存するのを防止
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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 = `
|
||||
|
||||
@@ -39,10 +39,23 @@
|
||||
|
||||
<br />
|
||||
|
||||
<button onclick="saveOpeningBalances()">保存</button>
|
||||
<button
|
||||
onclick="location.href = 'trial-balance.html'"
|
||||
class="back-button"
|
||||
onclick="saveOpeningBalances()"
|
||||
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="
|
||||
margin: 0 0 20px 0;
|
||||
padding: 8px 16px;
|
||||
@@ -54,7 +67,7 @@
|
||||
font-size: 14px;
|
||||
"
|
||||
>
|
||||
← 試算表に戻る
|
||||
← メインメニューに戻る
|
||||
</button>
|
||||
|
||||
<script src="js/api.js"></script>
|
||||
|
||||
@@ -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;
|
||||
|
||||
10
list_accounts.py
Normal file
10
list_accounts.py
Normal 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
108
verify_bank_balance.py
Normal 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()
|
||||
72
verify_opening_balances_table.py
Normal file
72
verify_opening_balances_table.py
Normal 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()
|
||||
Reference in New Issue
Block a user