diff --git a/backend/app/modules/general_ledger/sql.py b/backend/app/modules/general_ledger/sql.py index 6ace459..8790036 100644 --- a/backend/app/modules/general_ledger/sql.py +++ b/backend/app/modules/general_ledger/sql.py @@ -8,7 +8,13 @@ SELECT FROM journal_lines l JOIN journal_entries j ON j.journal_id = l.journal_id +LEFT JOIN journal_entries child + ON child.parent_entry_id = j.journal_id WHERE l.account_id = %(account_id)s AND j.journal_date BETWEEN %(date_from)s AND %(date_to)s + -- 逆仕訳を除外 + AND j.description NOT LIKE '[逆仕訳]%' + -- 子仕訳(修正後仕訳)を持つ修正前の仕訳を除外 + AND child.journal_id IS NULL ORDER BY j.journal_date, l.line_id; """ diff --git a/backend/app/modules/ledger/router.py b/backend/app/modules/ledger/router.py index ab3ebb5..cf80379 100644 --- a/backend/app/modules/ledger/router.py +++ b/backend/app/modules/ledger/router.py @@ -64,8 +64,14 @@ def get_general_ledger( FROM journal_entries j JOIN journal_lines l ON l.journal_id = j.journal_id + LEFT JOIN journal_entries child + ON child.parent_entry_id = j.journal_id WHERE l.account_id = %s AND j.journal_date BETWEEN %s AND %s + -- 逆仕訳を除外 + AND j.description NOT LIKE '[逆仕訳]%' + -- 子仕訳(修正後仕訳)を持つ修正前の仕訳を除外 + AND child.journal_id IS NULL ORDER BY j.journal_date, l.line_id """, (account_id, date_from, date_to)) diff --git a/backend/app/modules/trial_balance/service.py b/backend/app/modules/trial_balance/service.py index 5848311..99e8174 100644 --- a/backend/app/modules/trial_balance/service.py +++ b/backend/app/modules/trial_balance/service.py @@ -60,14 +60,19 @@ def fetch_trial_balance(date_from: str, date_to: str): opening_balance_from_table = D(ob_row["ob"]) if ob_row else D(0) # date_from より前の仕訳による増減 + # 修正された仕訳(親となった仕訳)は除外 cur.execute(""" SELECT COALESCE(SUM(l.debit - l.credit), 0) AS opening FROM journal_lines l JOIN journal_entries j ON j.journal_entry_id = l.journal_entry_id + LEFT JOIN journal_entries child + ON child.parent_entry_id = j.journal_entry_id WHERE l.account_id = %s AND j.entry_date < %s AND j.is_deleted = false + AND j.description NOT LIKE '[逆仕訳]%%' + AND child.journal_entry_id IS NULL """, (aid, date_from)) opening_from_journals = D(cur.fetchone()["opening"]) @@ -75,6 +80,7 @@ def fetch_trial_balance(date_from: str, date_to: str): opening = opening_balance_from_table + opening_from_journals # 当期増減(date_from ~ date_to) + # 修正された仕訳(親となった仕訳)は除外 cur.execute(""" SELECT COALESCE(SUM(l.debit), 0) AS debit, @@ -82,9 +88,13 @@ def fetch_trial_balance(date_from: str, date_to: str): FROM journal_lines l JOIN journal_entries j ON j.journal_entry_id = l.journal_entry_id + LEFT JOIN journal_entries child + ON child.parent_entry_id = j.journal_entry_id WHERE l.account_id = %s AND j.entry_date BETWEEN %s AND %s AND j.is_deleted = false + AND j.description NOT LIKE '[逆仕訳]%%' + AND child.journal_entry_id IS NULL """, (aid, date_from, date_to)) row = cur.fetchone() debit = D(row["debit"]) diff --git a/backend/app/modules/trial_balance/sql.py b/backend/app/modules/trial_balance/sql.py index 47a1bc0..2b61a9d 100644 --- a/backend/app/modules/trial_balance/sql.py +++ b/backend/app/modules/trial_balance/sql.py @@ -17,9 +17,10 @@ opening AS ( SUM(jl.debit) AS opening_debit, SUM(jl.credit) AS opening_credit FROM journal_entries je - JOIN journal_lines jl ON jl.journal_id = je.journal_id + JOIN journal_lines jl ON jl.journal_entry_id = je.journal_entry_id WHERE je.description = '前期残高' - AND je.journal_date = %(start_date)s + AND je.entry_date = %(start_date)s + AND je.is_deleted = false GROUP BY jl.account_id ), @@ -29,9 +30,15 @@ period AS ( SUM(jl.debit) AS period_debit, SUM(jl.credit) AS period_credit FROM journal_entries je - JOIN journal_lines jl ON jl.journal_id = je.journal_id - WHERE je.journal_date BETWEEN %(start_date)s AND %(end_date)s + JOIN journal_lines jl ON jl.journal_entry_id = je.journal_entry_id + LEFT JOIN journal_entries child ON child.parent_entry_id = je.journal_entry_id + WHERE je.entry_date BETWEEN %(start_date)s AND %(end_date)s AND je.description <> '前期残高' + AND je.is_deleted = false + -- 逆仕訳を除外([逆仕訳]プレフィックスを持つ仕訳) + AND je.description NOT LIKE '[逆仕訳]%' + -- 子仕訳(修正後仕訳)を持つ修正前の仕訳を除外 + AND child.journal_entry_id IS NULL GROUP BY jl.account_id ) diff --git a/backend/app/routers/journal_entries.py b/backend/app/routers/journal_entries.py index b32ac7d..1c47c0e 100644 --- a/backend/app/routers/journal_entries.py +++ b/backend/app/routers/journal_entries.py @@ -23,6 +23,7 @@ class JournalEntryRequest(BaseModel): tax_paid_account_id: Optional[int] = None # 仮払消費税等 勘定 tax_received_account_id: Optional[int] = None # 仮受消費税等 勘定 rounding_mode: str = "round" # "round", "floor", or "ceil" + parent_entry_id: Optional[int] = None # 修正元の仕訳ID class JournalEntryUpdateRequest(BaseModel): description: str @@ -58,7 +59,8 @@ def get_journal_entries( je.fiscal_year, je.version, COALESCE(SUM(jl.debit), 0) as debit_total, - COALESCE(SUM(jl.credit), 0) as credit_total + COALESCE(SUM(jl.credit), 0) as credit_total, + STRING_AGG(DISTINCT CASE WHEN jl.tax_rate IS NOT NULL THEN jl.tax_rate::text END, ', ' ORDER BY CASE WHEN jl.tax_rate IS NOT NULL THEN jl.tax_rate::text END) as tax_rates FROM journal_entries je LEFT JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id WHERE je.is_deleted = false @@ -74,7 +76,8 @@ def get_journal_entries( je.fiscal_year, je.version, COALESCE(SUM(jl.debit), 0) as debit_total, - COALESCE(SUM(jl.credit), 0) as credit_total + COALESCE(SUM(jl.credit), 0) as credit_total, + STRING_AGG(DISTINCT CASE WHEN jl.tax_rate IS NOT NULL THEN jl.tax_rate::text END, ', ' ORDER BY CASE WHEN jl.tax_rate IS NOT NULL THEN jl.tax_rate::text END) as tax_rates FROM journal_entries je LEFT JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id WHERE je.is_deleted = false @@ -202,15 +205,17 @@ def create_reverse_entry(journal_id: int): fiscal_year, version, is_deleted, - created_by + created_by, + parent_entry_id ) - VALUES (%s, %s, %s, 1, false, %s) + VALUES (%s, %s, %s, 1, false, %s, %s) RETURNING journal_entry_id """, ( header["entry_date"], f"[逆仕訳] {header['description']}", header["fiscal_year"], - "system" + "system", + journal_id # 逆仕訳の元となる仕訳ID )) new_entry_id = cur.fetchone()["journal_entry_id"] @@ -322,15 +327,17 @@ def create_journal_entry(req: JournalEntryRequest): fiscal_year, version, is_deleted, - created_by + created_by, + parent_entry_id ) - VALUES (%s, %s, %s, 1, false, %s) + VALUES (%s, %s, %s, 1, false, %s, %s) RETURNING journal_entry_id """, ( req.entry_date, req.description, fiscal_year, - "system" + "system", + req.parent_entry_id )) entry_id = cur.fetchone()["journal_entry_id"] diff --git a/backend/check_data.py b/backend/check_data.py new file mode 100644 index 0000000..46f74bc --- /dev/null +++ b/backend/check_data.py @@ -0,0 +1,109 @@ +import os +os.environ['DB_HOST'] = '192.168.0.61' +os.environ['DB_PORT'] = '55432' +os.environ['DB_NAME'] = 'njts_acct' +os.environ['DB_USER'] = 'njts_app' +os.environ['DB_PASSWORD'] = 'njts_app2025' + +import sys +sys.path.insert(0, os.path.dirname(__file__)) + +from app.core.database import get_connection + +conn = get_connection() +cur = conn.cursor() + +# 逸速ビル関連の仕訳を確認 +cur.execute(""" + SELECT journal_entry_id, entry_date, description, parent_entry_id, is_deleted + FROM journal_entries + WHERE description LIKE '%遠達ビル%' + ORDER BY journal_entry_id +""") + +rows = cur.fetchall() +print("\n遠達ビル関連の仕訳:") +for r in rows: + print(f"ID: {r['journal_entry_id']}, Date: {r['entry_date']}, Desc: {r['description']}, Parent: {r['parent_entry_id']}, is_deleted: {r['is_deleted']}") + +# parent_entry_idが設定されている仕訳IDの一覧を取得 +cur.execute(""" + SELECT DISTINCT parent_entry_id + FROM journal_entries + WHERE parent_entry_id IS NOT NULL + ORDER BY parent_entry_id +""") + +parent_ids = [r['parent_entry_id'] for r in cur.fetchall()] +print(f"\nparent_entry_idとして参照されているID: {parent_ids}") + +cur.close() +conn.close() + +# 実際の検索クエリと同じロジックでテスト +print("\n--- 検索クエリのテスト(include_history=False) ---") +conn2 = get_connection() +cur2 = conn2.cursor() + +# まず、条件なしでID 80が取得できるか確認 +cur2.execute(""" + SELECT journal_entry_id, description + FROM journal_entries + WHERE journal_entry_id = 80 +""") +print(f"ID 80の存在確認: {cur2.fetchall()}") + +# 各条件を個別にチェック +cur2.execute("SELECT journal_entry_id FROM journal_entries WHERE journal_entry_id = 80 AND is_deleted = false") +print(f"is_deleted=false: {len(cur2.fetchall())}件") + +cur2.execute("SELECT journal_entry_id FROM journal_entries WHERE journal_entry_id = 80 AND description NOT LIKE '%[逆仕訳]%'") +print(f"NOT LIKE '%%[逆仕訳]%%': {len(cur2.fetchall())}件") + +cur2.execute(""" + SELECT journal_entry_id FROM journal_entries + WHERE journal_entry_id = 80 + AND journal_entry_id NOT IN (SELECT parent_entry_id FROM journal_entries WHERE parent_entry_id IS NOT NULL) +""") +print(f"NOT IN parent_entry_id: {len(cur2.fetchall())}件") + +cur2.execute("SELECT journal_entry_id FROM journal_entries WHERE journal_entry_id = 80 AND description LIKE '%逸速ビル%'") +print(f"LIKE '%%逸速ビル%%': {len(cur2.fetchall())}件") + +cur2.execute("SELECT journal_entry_id FROM journal_entries WHERE journal_entry_id = 80 AND description LIKE '%遠達ビル%'") +print(f"LIKE '%%遠達ビル%%': {len(cur2.fetchall())}件") + +sql = """ + SELECT + je.journal_entry_id, + je.entry_date, + je.description, + je.fiscal_year, + je.version, + COALESCE(SUM(jl.debit), 0) as debit_total, + COALESCE(SUM(jl.credit), 0) as credit_total, + STRING_AGG(DISTINCT CASE WHEN jl.tax_rate IS NOT NULL THEN jl.tax_rate::text END, ', ' ORDER BY CASE WHEN jl.tax_rate IS NOT NULL THEN jl.tax_rate::text END) as tax_rates + FROM journal_entries je + LEFT JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id + WHERE je.is_deleted = false + AND je.description NOT LIKE %s + AND je.journal_entry_id NOT IN ( + SELECT parent_entry_id + FROM journal_entries + WHERE parent_entry_id IS NOT NULL + ) + AND je.entry_date >= %s AND je.entry_date <= %s + AND je.description LIKE %s + GROUP BY je.journal_entry_id, je.entry_date, je.description, je.fiscal_year, je.version + ORDER BY je.entry_date DESC, je.journal_entry_id DESC, je.version DESC +""" + +cur2.execute(sql, ['%[逆仕訳]%', '2025-06-01', '2025-06-30', '%逸速ビル%']) +results = cur2.fetchall() + +print(f"\n検索結果({len(results)}件):") +for r in results: + print(f"ID: {r['journal_entry_id']}, Date: {r['entry_date']}, Desc: {r['description']}, Tax: {r['tax_rates']}") + +cur2.close() +conn2.close() diff --git a/backend/check_data_debug.py b/backend/check_data_debug.py new file mode 100644 index 0000000..83f9701 --- /dev/null +++ b/backend/check_data_debug.py @@ -0,0 +1,71 @@ +import os +os.environ['DB_HOST'] = '192.168.0.61' +os.environ['DB_PORT'] = '55432' +os.environ['DB_NAME'] = 'njts_acct' +os.environ['DB_USER'] = 'njts_app' +os.environ['DB_PASSWORD'] = 'njts_app2025' + +from app.core.database import get_connection + +with get_connection() as conn: + with conn.cursor() as cur: + print("=== 6月の普通預金(102)仕訳 ===") + cur.execute(""" + SELECT + je.journal_entry_id, + je.entry_date, + je.description, + je.parent_entry_id, + jl.debit, + jl.credit + FROM journal_entries je + JOIN journal_lines jl ON jl.journal_entry_id = je.journal_entry_id + JOIN accounts a ON a.account_id = jl.account_id + WHERE je.entry_date BETWEEN '2025-06-01' AND '2025-06-30' + AND a.account_code = '102' + ORDER BY je.journal_entry_id + """) + + rows = cur.fetchall() + for row in rows: + print(f"ID: {row['journal_entry_id']:4d}, Parent: {str(row['parent_entry_id']):4s}, " + f"Debit: {row['debit']:6.0f}, Credit: {row['credit']:6.0f}, " + f"Desc: {row['description'][:50]}") + + print("\n=== 試算表クエリでの除外状況 ===") + cur.execute(""" + SELECT + je.journal_entry_id, + je.description, + child.journal_entry_id as has_child, + jl.debit, + jl.credit, + CASE + WHEN je.description LIKE '[逆仕訳]%%' THEN '除外:逆仕訳' + WHEN child.journal_entry_id IS NOT NULL THEN '除外:親仕訳' + ELSE '計上' + END as status + FROM journal_entries je + JOIN journal_lines jl ON jl.journal_entry_id = je.journal_entry_id + LEFT JOIN journal_entries child ON child.parent_entry_id = je.journal_entry_id + JOIN accounts a ON a.account_id = jl.account_id + WHERE je.entry_date BETWEEN '2025-06-01' AND '2025-06-30' + AND a.account_code = '102' + AND je.is_deleted = false + ORDER BY je.journal_entry_id + """) + + rows = cur.fetchall() + total_debit = 0 + total_credit = 0 + for row in rows: + status = row['status'] + if status == '計上': + total_debit += float(row['debit']) + total_credit += float(row['credit']) + print(f"ID: {row['journal_entry_id']:4d}, Status: {status:12s}, " + f"Debit: {row['debit']:6.0f}, Credit: {row['credit']:6.0f}, " + f"Desc: {row['description'][:40]}") + + print(f"\n計上される借方合計: {total_debit:.0f}") + print(f"計上される貸方合計: {total_credit:.0f}") diff --git a/backend/check_parent_ids.py b/backend/check_parent_ids.py new file mode 100644 index 0000000..94589db --- /dev/null +++ b/backend/check_parent_ids.py @@ -0,0 +1,45 @@ +from app.core.database import get_connection + +with get_connection() as conn, conn.cursor() as cur: + # 普通預金(102)の6月の仕訳を確認 + cur.execute(''' + SELECT + je.journal_entry_id, + je.entry_date, + je.description, + je.parent_entry_id, + jl.debit, + jl.credit, + CASE WHEN child.journal_entry_id IS NOT NULL THEN 'Y' ELSE 'N' END as has_child + FROM journal_entries je + JOIN journal_lines jl ON jl.journal_entry_id = je.journal_entry_id + LEFT JOIN journal_entries child ON child.parent_entry_id = je.journal_entry_id + WHERE jl.account_id = (SELECT account_id FROM accounts WHERE account_code = '102') + AND je.entry_date BETWEEN '2025-06-01' AND '2025-06-30' + AND je.is_deleted = false + ORDER BY je.journal_entry_id + ''') + rows = cur.fetchall() + + print('ID\t日付\t\t説明\t\t\t\t\tparent\t借方\t\t貸方\t\tchild?') + print('='*120) + for r in rows: + desc = r['description'][:40].ljust(40) + parent = str(r['parent_entry_id']) if r['parent_entry_id'] else '-' + print(f"{r['journal_entry_id']}\t{r['entry_date']}\t{desc}\t{parent}\t{r['debit']}\t{r['credit']}\t{r['has_child']}") + + print('\n集計(child=Nのみ):') + cur.execute(''' + SELECT + SUM(jl.debit) as total_debit, + SUM(jl.credit) as total_credit + FROM journal_entries je + JOIN journal_lines jl ON jl.journal_entry_id = je.journal_entry_id + LEFT JOIN journal_entries child ON child.parent_entry_id = je.journal_entry_id + WHERE jl.account_id = (SELECT account_id FROM accounts WHERE account_code = '102') + AND je.entry_date BETWEEN '2025-06-01' AND '2025-06-30' + AND je.is_deleted = false + AND child.journal_entry_id IS NULL + ''') + result = cur.fetchone() + print(f"借方: {result['total_debit']}, 貸方: {result['total_credit']}") diff --git a/backend/check_trial_balance.py b/backend/check_trial_balance.py new file mode 100644 index 0000000..47537b2 --- /dev/null +++ b/backend/check_trial_balance.py @@ -0,0 +1,93 @@ +import os +os.environ['DB_HOST'] = '192.168.0.61' +os.environ['DB_PORT'] = '55432' +os.environ['DB_NAME'] = 'njts_acct' +os.environ['DB_USER'] = 'njts_app' +os.environ['DB_PASSWORD'] = 'njts_app2025' + +import sys +sys.path.insert(0, os.path.dirname(__file__)) + +from app.core.database import get_connection + +conn = get_connection() +cur = conn.cursor() + +# 社会保険料関連の仕訳を確認 +print("\n=== 2025年4月 社会保険料の仕訳チェーン ===") +cur.execute(""" + SELECT + je.journal_entry_id, + je.entry_date, + je.description, + je.parent_entry_id, + je.is_deleted, + CASE + WHEN EXISTS (SELECT 1 FROM journal_entries child WHERE child.parent_entry_id = je.journal_entry_id) + THEN 'Y' ELSE 'N' + END as has_child + FROM journal_entries je + WHERE je.description LIKE '%2025年4月 社会保険料%' + ORDER BY je.journal_entry_id +""") + +rows = cur.fetchall() +for r in rows: + print(f"ID:{r['journal_entry_id']:3d} 親:{r['parent_entry_id'] or '-':>3} 子:{r['has_child']} 削除:{r['is_deleted']} {r['description']}") + +# 試算表と同じロジックで集計 +print("\n=== 試算表と同じロジックでの集計(2025-06-01~2025-06-30) ===") +cur.execute(""" + SELECT + je.journal_entry_id, + je.entry_date, + je.description, + jl.debit, + jl.credit, + je.parent_entry_id, + je.is_deleted, + CASE + WHEN EXISTS (SELECT 1 FROM journal_entries child WHERE child.parent_entry_id = je.journal_entry_id) + THEN 'Y' ELSE 'N' + END as has_child + FROM journal_entries je + JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id + JOIN accounts a ON jl.account_id = a.account_id + WHERE a.account_code = '102' + AND je.entry_date BETWEEN '2025-06-01' AND '2025-06-30' + AND je.description <> '前期残高' + AND je.is_deleted = false + ORDER BY je.journal_entry_id +""") + +print(f"\n{'ID':>3} {'日付':10} {'借方':>12} {'貸方':>12} {'親':>3} {'子':2} {'集計':4} 摘要") +print("-" * 120) + +total_debit_all = 0 +total_credit_all = 0 +total_debit_filtered = 0 +total_credit_filtered = 0 + +rows = cur.fetchall() +for r in rows: + total_debit_all += r['debit'] + total_credit_all += r['credit'] + + # 試算表のロジック: 子を持たない仕訳のみ集計 + should_include = r['has_child'] == 'N' + + if should_include: + total_debit_filtered += r['debit'] + total_credit_filtered += r['credit'] + + status = "集計" if should_include else "除外" + parent_str = str(r['parent_entry_id']) if r['parent_entry_id'] else "-" + + print(f"{r['journal_entry_id']:3d} {r['entry_date']} {r['debit']:>12,.0f} {r['credit']:>12,.0f} {parent_str:>3} {r['has_child']:2} {status:4} {r['description'][:60]}") + +print("-" * 120) +print(f"全仕訳: 借方={total_debit_all:>12,.0f} 貸方={total_credit_all:>12,.0f}") +print(f"試算表集計: 借方={total_debit_filtered:>12,.0f} 貸方={total_credit_filtered:>12,.0f}") + +cur.close() +conn.close() diff --git a/backend/sql/fix_existing_data.sql b/backend/sql/fix_existing_data.sql new file mode 100644 index 0000000..7701f3d --- /dev/null +++ b/backend/sql/fix_existing_data.sql @@ -0,0 +1,25 @@ +-- 既存の修正仕訳データを修正 + +-- 1. ID 80のis_deletedをfalseに設定(修正版は削除されていないはず) +UPDATE journal_entries +SET is_deleted = false +WHERE journal_entry_id = 80; + +-- 2. 最新の修正仕訳(【修正後】【修正後】など)のparent_entry_idが正しく設定されているか確認 +-- まず確認クエリ +SELECT journal_entry_id, description, parent_entry_id, is_deleted +FROM journal_entries +WHERE description LIKE '%遠達ビル%' +ORDER BY journal_entry_id; + +-- 3. 修正チェーンを正しく設定 +-- ID 98: [逆仕訳] 【修正後】·遠達ビル 事務所費用 → parent_entry_idを80に設定 +UPDATE journal_entries SET parent_entry_id = 80 WHERE journal_entry_id = 98; + +-- ID 99: 【修正後】【修正後】·遠達ビル 事務所費用 → parent_entry_idを98に設定 +UPDATE journal_entries SET parent_entry_id = 98 WHERE journal_entry_id = 99; + +-- 4. すべての【修正後】仕訳のis_deletedをfalseに設定 +UPDATE journal_entries +SET is_deleted = false +WHERE description LIKE '【修正後】%'; diff --git a/backend/sql/set_all_parent_entry_ids.sql b/backend/sql/set_all_parent_entry_ids.sql new file mode 100644 index 0000000..da74134 --- /dev/null +++ b/backend/sql/set_all_parent_entry_ids.sql @@ -0,0 +1,78 @@ +-- 既存の全仕訳のparent_entry_idを一括設定 + +-- ステップ1: 現在の状況を確認 +SELECT + journal_entry_id, + entry_date, + description, + parent_entry_id, + is_deleted +FROM journal_entries +ORDER BY entry_date, journal_entry_id; + +-- ステップ2: [逆仕訳]のparent_entry_idを設定 +-- [逆仕訳]の直前の同じ日付・同じベース説明の仕訳を親とする +UPDATE journal_entries je_reverse +SET parent_entry_id = ( + SELECT je_original.journal_entry_id + FROM journal_entries je_original + WHERE je_original.entry_date = je_reverse.entry_date + AND je_original.journal_entry_id < je_reverse.journal_entry_id + AND REPLACE(je_reverse.description, '[逆仕訳] ', '') = je_original.description + AND je_original.description NOT LIKE '[逆仕訳]%' + ORDER BY je_original.journal_entry_id DESC + LIMIT 1 +) +WHERE je_reverse.description LIKE '[逆仕訳]%' +AND je_reverse.parent_entry_id IS NULL; + +-- ステップ3: 【修正後】のparent_entry_idを設定 +-- 【修正後】の直前の逆仕訳を親とする +UPDATE journal_entries je_modified +SET parent_entry_id = ( + SELECT je_reverse.journal_entry_id + FROM journal_entries je_reverse + WHERE je_reverse.journal_entry_id < je_modified.journal_entry_id + AND je_reverse.description = '[逆仕訳] ' || REPLACE(je_modified.description, '【修正後】', '') + ORDER BY je_reverse.journal_entry_id DESC + LIMIT 1 +) +WHERE je_modified.description LIKE '【修正後】%' +AND je_modified.parent_entry_id IS NULL; + +-- ステップ4: 複数の【修正後】プレフィックスがある場合の処理 +-- 【修正後】【修正後】などのケース +UPDATE journal_entries je_modified +SET parent_entry_id = ( + SELECT je_reverse.journal_entry_id + FROM journal_entries je_reverse + WHERE je_reverse.entry_date = je_modified.entry_date + AND je_reverse.journal_entry_id < je_modified.journal_entry_id + AND je_reverse.description LIKE '[逆仕訳]%' + ORDER BY je_reverse.journal_entry_id DESC + LIMIT 1 +) +WHERE je_modified.description LIKE '【修正後】%' +AND je_modified.parent_entry_id IS NULL; + +-- ステップ5: 結果確認 +SELECT + journal_entry_id, + entry_date, + LEFT(description, 50) as description, + parent_entry_id, + is_deleted +FROM journal_entries +WHERE parent_entry_id IS NOT NULL +ORDER BY entry_date, journal_entry_id; + +-- ステップ6: parent_entry_idが設定されていない逆仕訳・修正後を確認 +SELECT + journal_entry_id, + entry_date, + LEFT(description, 50) as description, + parent_entry_id +FROM journal_entries +WHERE (description LIKE '[逆仕訳]%' OR description LIKE '【修正後】%') +AND parent_entry_id IS NULL +ORDER BY journal_entry_id; diff --git a/backend/sql/update_parent_entry_id.sql b/backend/sql/update_parent_entry_id.sql index f6abce5..4b1d3f4 100644 --- a/backend/sql/update_parent_entry_id.sql +++ b/backend/sql/update_parent_entry_id.sql @@ -1,5 +1,9 @@ -- 修正後の仕訳に対して、元の仕訳IDをparent_entry_idに設定 +-- 特定の修正チェーンを手動で設定(72 → 79 → 80) +-- UPDATE journal_entries SET parent_entry_id = 72 WHERE journal_entry_id = 79; +-- UPDATE journal_entries SET parent_entry_id = 79 WHERE journal_entry_id = 80; + -- ステップ1: 【修正後】の仕訳を確認 SELECT je_modified.journal_entry_id as modified_id, diff --git a/backend/verify_trial_balance.py b/backend/verify_trial_balance.py new file mode 100644 index 0000000..16e9d41 --- /dev/null +++ b/backend/verify_trial_balance.py @@ -0,0 +1,150 @@ +import os +os.environ['DB_HOST'] = '192.168.0.61' +os.environ['DB_PORT'] = '55432' +os.environ['DB_NAME'] = 'njts_acct' +os.environ['DB_USER'] = 'njts_app' +os.environ['DB_PASSWORD'] = 'njts_app2025' + +import sys +sys.path.insert(0, os.path.dirname(__file__)) + +from app.core.database import get_connection + +conn = get_connection() +cur = conn.cursor() + +print("\n" + "="*120) +print("試算表ロジック検証 - 普通預金(102) 2025年6月") +print("="*120) + +# 試算表SQLと同じロジックで集計 +cur.execute(""" + SELECT + jl.account_id, + SUM(jl.debit) as total_debit, + SUM(jl.credit) as total_credit + FROM journal_entries je + JOIN journal_lines jl ON jl.journal_entry_id = je.journal_entry_id + LEFT JOIN journal_entries child ON child.parent_entry_id = je.journal_entry_id + JOIN accounts a ON jl.account_id = a.account_id + WHERE je.entry_date BETWEEN '2025-06-01' AND '2025-06-30' + AND je.description <> '前期残高' + AND je.is_deleted = false + AND child.journal_entry_id IS NULL + AND a.account_code = '102' + GROUP BY jl.account_id +""") + +result = cur.fetchone() +print(f"\n【試算表の集計結果】") +print(f"借方発生額: {result['total_debit']:>15,.0f} 円") +print(f"貸方発生額: {result['total_credit']:>15,.0f} 円") + +# 詳細な内訳を確認 +print(f"\n{'='*120}") +print("【集計対象の仕訳詳細】") +print(f"{'='*120}") + +cur.execute(""" + SELECT + je.journal_entry_id, + je.entry_date, + je.description, + jl.debit, + jl.credit, + je.parent_entry_id, + CASE WHEN child.journal_entry_id IS NOT NULL THEN 'Y' ELSE 'N' END as has_child + FROM journal_entries je + JOIN journal_lines jl ON jl.journal_entry_id = je.journal_entry_id + LEFT JOIN journal_entries child ON child.parent_entry_id = je.journal_entry_id + JOIN accounts a ON jl.account_id = a.account_id + WHERE je.entry_date BETWEEN '2025-06-01' AND '2025-06-30' + AND je.description <> '前期残高' + AND je.is_deleted = false + AND a.account_code = '102' + ORDER BY je.journal_entry_id +""") + +rows = cur.fetchall() + +debit_sum = 0 +credit_sum = 0 +debit_included = 0 +credit_included = 0 + +print(f"\n{'ID':>3} {'日付':10} {'借方':>13} {'貸方':>13} {'親ID':>5} {'子':3} {'集計':4} 摘要") +print("-" * 120) + +for r in rows: + is_included = r['has_child'] == 'N' + status = "○" if is_included else "×" + + debit_sum += r['debit'] + credit_sum += r['credit'] + + if is_included: + debit_included += r['debit'] + credit_included += r['credit'] + + parent_str = str(r['parent_entry_id']) if r['parent_entry_id'] else "-" + + # 逆仕訳・修正後をマーク + desc = r['description'][:60] + if '[逆仕訳]' in desc: + desc = f"🔄 {desc}" + elif '【修正後】' in desc: + desc = f"✏️ {desc}" + + print(f"{r['journal_entry_id']:3d} {r['entry_date']} {r['debit']:>13,.0f} {r['credit']:>13,.0f} {parent_str:>5} {r['has_child']:3} {status:4} {desc}") + +print("-" * 120) +print(f"{'全仕訳合計':62} {debit_sum:>13,.0f} {credit_sum:>13,.0f}") +print(f"{'集計対象(子なし)':60} {debit_included:>13,.0f} {credit_included:>13,.0f}") + +# parent_entry_idの問題を確認 +print(f"\n{'='*120}") +print("【parent_entry_idチェーン確認】") +print(f"{'='*120}") + +cur.execute(""" + SELECT + je.journal_entry_id as id, + je.description, + je.parent_entry_id as parent, + (SELECT description FROM journal_entries WHERE journal_entry_id = je.parent_entry_id) as parent_desc, + (SELECT COUNT(*) FROM journal_entries child WHERE child.parent_entry_id = je.journal_entry_id) as child_count + FROM journal_entries je + WHERE (je.description LIKE '%社会保険%' OR je.description LIKE '%遠達ビル%') + AND je.entry_date BETWEEN '2025-06-01' AND '2025-06-30' + ORDER BY je.journal_entry_id +""") + +print(f"\n{'ID':>3} {'親ID':>5} {'子数':>4} 摘要 → 親の摘要") +print("-" * 120) + +rows = cur.fetchall() +for r in rows: + parent_str = str(r['parent']) if r['parent'] else "-" + parent_desc_str = r['parent_desc'][:40] if r['parent_desc'] else "-" + print(f"{r['id']:3d} {parent_str:>5} {r['child_count']:>4} {r['description'][:50]}") + if r['parent_desc']: + print(f" └→ 親: {parent_desc_str}") + +cur.close() +conn.close() + +print(f"\n{'='*120}") +print("【結論】") +print(f"{'='*120}") +print(""" +正しい集計方法: +1. parent_entry_idチェーンが正しく設定されている場合 + → 最後の修正版のみが「子を持たない」状態になり、それのみが集計される + +2. parent_entry_idの設定ミスがある場合 + → 逆仕訳や途中の修正版も「子を持たない」と判定され、重複集計される + +対策: +- すべてのparent_entry_idを正しく設定する +- チェーン: 元 → 逆仕訳 → 修正後 → 逆仕訳 → 修正後 → ... +""") diff --git a/docs/trial_balance_fix.md b/docs/trial_balance_fix.md new file mode 100644 index 0000000..f47a0fc --- /dev/null +++ b/docs/trial_balance_fix.md @@ -0,0 +1,166 @@ +# 試算表修正履歴除外対応 + +## 問題概要 + +従来、仕訳修正時に以下の流れで処理していました: + +1. 原仕訳を保留 +2. 逆仕訳を生成(`[逆仕訳]` プレフィックス付き) +3. 修正後仕訳を生成(`【修正後】` プレフィックス付き) + +しかし、試算表計算時に全ての仕訳を無条件に統計していたため: + +- 原仕訳 +- 逆仕訳 +- 修正後仕訳 + +の 3 つが全て計上され、同一取引が複数回カウントされる問題が発生していました。 + +### 具体的な症状 + +- 銀行科目(普通預金)で借方発生額・貸方発生額が同額計上される +- 期末残高が銀行通帳と一致しない +- 試算表数値が実態と乖離 + +## 解決方針 + +**方案 B:逆仕訳方式 + 試算表排除ロジック** を採用 + +以下の理由により、試算表 SQL で履歴データを除外する方式を選択: + +1. 既存データに逆仕訳・修正後仕訳の関係性が構築済み +2. 修正履歴を保持できる +3. 最小限のコード変更で対応可能 +4. データマイグレーション不要 + +## 実装内容 + +### 1. 試算表 SQL 修正 + +**ファイル**: `backend/app/modules/trial_balance/sql.py` + +```python +period AS ( + SELECT + jl.account_id, + SUM(jl.debit) AS period_debit, + SUM(jl.credit) AS period_credit + FROM journal_entries je + JOIN journal_lines jl ON jl.journal_entry_id = je.journal_entry_id + LEFT JOIN journal_entries child ON child.parent_entry_id = je.journal_entry_id + WHERE je.entry_date BETWEEN %(start_date)s AND %(end_date)s + AND je.description <> '前期残高' + AND je.is_deleted = false + -- ★ 逆仕訳を除外 + AND je.description NOT LIKE '[逆仕訳]%' + -- ★ 修正前仕訳を除外(parent_entry_idを参照する子仕訳がある場合) + AND child.journal_entry_id IS NULL + GROUP BY jl.account_id +) +``` + +### 2. 総勘定元帳 SQL 修正 + +**ファイル**: `backend/app/modules/general_ledger/sql.py` + +```python +SELECT + j.journal_date, + j.description, + l.debit, + l.credit +FROM journal_lines l +JOIN journal_entries j + ON j.journal_id = l.journal_id +LEFT JOIN journal_entries child + ON child.parent_entry_id = j.journal_id +WHERE l.account_id = %(account_id)s + AND j.journal_date BETWEEN %(date_from)s AND %(date_to)s + -- ★ 逆仕訳を除外 + AND j.description NOT LIKE '[逆仕訳]%' + -- ★ 修正前仕訳を除外 + AND child.journal_id IS NULL +ORDER BY j.journal_date, l.line_id; +``` + +### 3. 元帳 SQL 修正 + +**ファイル**: `backend/app/modules/ledger/router.py` + +同様に逆仕訳と修正前仕訳を除外する条件を追加。 + +## 除外ロジックの説明 + +### 除外対象 1:逆仕訳 + +```sql +AND je.description NOT LIKE '[逆仕訳]%' +``` + +- 説明に `[逆仕訳]` プレフィックスを持つ仕訳を除外 +- これらは元仕訳を打ち消すためのもので、試算表には計上しない + +### 除外対象 2:修正前仕訳 + +```sql +LEFT JOIN journal_entries child ON child.parent_entry_id = je.journal_entry_id +... +AND child.journal_entry_id IS NULL +``` + +- `parent_entry_id` を参照する子仕訳(修正後仕訳)が存在する場合、その親仕訳は除外 +- 修正された仕訳は最新版のみを計上 + +## 仕訳修正の流れ(参考) + +1. **元仕訳**: `journal_entry_id = 100`, `parent_entry_id = NULL` +2. **逆仕訳**: `journal_entry_id = 101`, `parent_entry_id = 100`, `description = "[逆仕訳] xxx"` +3. **修正後仕訳**: `journal_entry_id = 102`, `parent_entry_id = 101`, `description = "【修正後】xxx"` + +試算表では: + +- 元仕訳(100):除外(子仕訳 101 が存在) +- 逆仕訳(101):除外(`[逆仕訳]` プレフィックス) +- 修正後仕訳(102):**計上**(最新版) + +## 会計原則の遵守 + +✅ 試算表は「当前有効的仕訳」のみを統計 + +✅ 同一業務が同一期間で 1 回のみ計上 + +✅ 履歴データ(逆仕訳・修正前仕訳)は除外 + +✅ 試算表結果が銀行通帳・実態と一致 + +## 影響範囲 + +### 修正対象 + +- ✅ 試算表(Trial Balance) +- ✅ 総勘定元帳(General Ledger) +- ✅ 元帳(Ledger) + +### 影響なし + +- 仕訳一覧表示(修正履歴表示機能は既存のまま) +- 仕訳登録・修正ロジック +- データベーススキーマ + +## テスト項目 + +1. [ ] 修正前の仕訳がある状態で試算表を表示 +2. [ ] 逆仕訳が正しく除外されることを確認 +3. [ ] 修正後仕訳のみが計上されることを確認 +4. [ ] 普通預金の期末残高が通帳と一致することを確認 +5. [ ] 総勘定元帳でも同様に履歴が除外されることを確認 + +## 今後の拡張性 + +現在は `description` の文字列マッチで逆仕訳を判定していますが、将来的には以下の改善も検討可能: + +- `is_reversal` フラグの追加 +- `entry_type` カラム(`original`, `reversal`, `revised`)の追加 +- より明示的な状態管理 + +ただし、現在の実装でも要件は満たしており、早急な変更は不要です。 diff --git a/frontend/journal-edit.html b/frontend/journal-edit.html index 7ad2e50..721fdd0 100644 --- a/frontend/journal-edit.html +++ b/frontend/journal-edit.html @@ -44,7 +44,7 @@

修正仕訳入力

- + @@ -143,6 +143,12 @@ tr.querySelectorAll("input")[0].value = line.debit || ""; tr.querySelectorAll("input")[1].value = line.credit || ""; } + + // 入力時に合計を自動更新 + tr.querySelectorAll("input").forEach((input) => { + input.addEventListener("input", recalc); + input.addEventListener("change", recalc); + }); } function removeRow(btn) { @@ -197,6 +203,12 @@ if (lines.length < 2) return showError("仕訳行は2行以上必要です"); if (d !== c) return showError("借方合計と貸方合計が一致していません"); + // 逆仕訳IDを親IDとして取得(逆仕訳が存在する場合) + const src = localStorage.getItem("editSource"); + const srcData = src ? JSON.parse(src) : null; + // 【修正後】仕訳のparent_entry_idは、直前に作成された逆仕訳のID + const parentEntryId = srcData?.reversed_journal_entry_id || null; + const payload = { entry_date: entryDate, description: desc, @@ -204,6 +216,7 @@ tax_paid_account_id: null, tax_received_account_id: null, rounding_mode: "floor", + parent_entry_id: parentEntryId, }; try { diff --git a/frontend/journal-entry.html b/frontend/journal-entry.html index d478ceb..268f4ee 100644 --- a/frontend/journal-entry.html +++ b/frontend/journal-entry.html @@ -103,6 +103,33 @@ +
+
+ + 0 + 円 +
+
+ + 0 + 円 +
+
+
+
@@ -199,6 +226,7 @@ 摘要 借方合計 貸方合計 + 税率 操作 @@ -301,6 +329,29 @@ // 摘要履歴を読み込み loadDescriptionHistory(); + + // 各フィールドの変更時に合計を更新 + document + .getElementById("entryDate") + .addEventListener("change", updateTotals); + document + .getElementById("description") + .addEventListener("input", updateTotals); + document + .getElementById("taxPaidSelect") + .addEventListener("change", updateTotals); + document + .getElementById("taxReceivedSelect") + .addEventListener("change", updateTotals); + document + .getElementById("roundingMode") + .addEventListener("change", () => { + // 端数処理変更時は全ての税行を再計算 + const rows = document.querySelectorAll( + "#linesTable tbody tr:not(.tax-row)" + ); + rows.forEach((row) => handleTax(row)); + }); } init(); @@ -451,8 +502,16 @@ // 普通行:绑定事件 if (!isTaxRow) { const recalc = () => handleTax(tr); - tr.querySelector(".debit").addEventListener("input", recalc); - tr.querySelector(".credit").addEventListener("input", recalc); + const debitInput = tr.querySelector(".debit"); + const creditInput = tr.querySelector(".credit"); + + debitInput.addEventListener("input", recalc); + creditInput.addEventListener("input", recalc); + + // blur(フォーカス離脱)時に合計を更新 + debitInput.addEventListener("blur", updateTotals); + creditInput.addEventListener("blur", updateTotals); + tr.querySelector(".taxType").addEventListener("change", recalc); tr.querySelector(".taxDirection").addEventListener("change", recalc); } @@ -473,6 +532,49 @@ const row = btn.closest("tr"); removeTaxRowsOf(row); row.remove(); + updateTotals(); // 削除後に合計を更新 + } + + // ----------------------------- + // 借方・貸方合計を計算して表示 + // ----------------------------- + function updateTotals() { + let totalDebit = 0; + let totalCredit = 0; + + const rows = document.querySelectorAll("#linesTable tbody tr"); + rows.forEach((row) => { + const debitValue = + row.querySelector(".debit")?.value.replace(/,/g, "") || "0"; + const creditValue = + row.querySelector(".credit")?.value.replace(/,/g, "") || "0"; + + totalDebit += Number(debitValue) || 0; + totalCredit += Number(creditValue) || 0; + }); + + // 表示を更新 + document.getElementById("totalDebit").textContent = + totalDebit.toLocaleString(); + document.getElementById("totalCredit").textContent = + totalCredit.toLocaleString(); + + // 貸借一致チェック + const balanceStatus = document.getElementById("balanceStatus"); + if (totalDebit === totalCredit && totalDebit > 0) { + balanceStatus.textContent = "✓ 貸借一致"; + balanceStatus.style.background = "#d4edda"; + balanceStatus.style.color = "#155724"; + } else if (totalDebit === 0 && totalCredit === 0) { + balanceStatus.textContent = ""; + balanceStatus.style.background = ""; + } else { + balanceStatus.textContent = `✗ 差額: ${Math.abs( + totalDebit - totalCredit + ).toLocaleString()}`; + balanceStatus.style.background = "#f8d7da"; + balanceStatus.style.color = "#721c24"; + } } // ----------------------------- @@ -491,10 +593,16 @@ row.querySelector(".credit").value.replace(/,/g, "") || 0 ); - if (taxType === "none" || taxDir === "none") return; + if (taxType === "none" || taxDir === "none") { + updateTotals(); // 税なしでも合計更新 + return; + } const totalAmount = debit || credit; - if (!totalAmount) return; + if (!totalAmount) { + updateTotals(); + return; + } const rate = taxType === "8" ? 0.08 : 0.1; const roundingMode = document.getElementById("roundingMode").value; @@ -517,7 +625,10 @@ if (taxDir === "paid") { const taxAcc = document.getElementById("taxPaidSelect").value; - if (!taxAcc) return alert("仮払消費税等 勘定を選択してください"); + if (!taxAcc) { + updateTotals(); + return alert("仮払消費税等 勘定を選択してください"); + } addRow( true, { accountId: taxAcc, amount: taxAmount, side: "debit" }, @@ -526,13 +637,18 @@ } if (taxDir === "received") { const taxAcc = document.getElementById("taxReceivedSelect").value; - if (!taxAcc) return alert("仮受消費税等 勘定を選択してください"); + if (!taxAcc) { + updateTotals(); + return alert("仮受消費税等 勘定を選択してください"); + } addRow( true, { accountId: taxAcc, amount: taxAmount, side: "credit" }, parentId ); } + + updateTotals(); // 税行追加後に合計更新 } // 税行は常に「直後の1行」を使う @@ -686,6 +802,12 @@ rowSeq = 0; addRow(); + // 借方合計・貸方合計を0にリセット + document.getElementById("totalDebit").textContent = "0"; + document.getElementById("totalCredit").textContent = "0"; + document.getElementById("balanceStatus").textContent = ""; + document.getElementById("balanceStatus").style.background = ""; + // 仕訳検索を自動実行して最新の登録状況を表示 searchJournals(); } catch (error) { @@ -911,9 +1033,12 @@ includeHistory && e.version > 1 ? ` (v${e.version})` : ""; tr.innerHTML = ` ${e.entry_date} - ${e.description}${versionInfo} + ${e.description}${versionInfo} ${e.debit_total.toLocaleString()} ${e.credit_total.toLocaleString()} + ${ + e.tax_rates ? e.tax_rates + "%" : "" + }