diff --git a/_check_compose.py b/_check_compose.py new file mode 100644 index 0000000..afc4d55 --- /dev/null +++ b/_check_compose.py @@ -0,0 +1,16 @@ +import paramiko +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect('192.168.0.61', username='root', password='59911784', timeout=15) + +# NAS上の実際のdocker-compose.ymlを確認 +_, o, _ = c.exec_command('cat /volume1/docker/njts-accounting/docker-compose.yml') +print("=== docker-compose.yml ===") +print(o.read().decode()) + +# コンテナの全マウントを確認 +_, o2, _ = c.exec_command('docker inspect njts-accounting-backend --format "{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}"') +print("=== マウント一覧 ===") +print(o2.read().decode()) + +c.close() diff --git a/_check_containers.py b/_check_containers.py new file mode 100644 index 0000000..70ebaed --- /dev/null +++ b/_check_containers.py @@ -0,0 +1,7 @@ +import paramiko +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect('192.168.0.61', username='root', password='59911784', timeout=15) +_, o, _ = c.exec_command('cd /volume1/docker/njts-accounting && docker compose ps') +print(o.read().decode()) +c.close() diff --git a/_check_mount.py b/_check_mount.py new file mode 100644 index 0000000..fcf5e48 --- /dev/null +++ b/_check_mount.py @@ -0,0 +1,15 @@ +import paramiko +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect('192.168.0.61', username='root', password='59911784', timeout=15) +# コンテナ内の /app/frontend の内容確認 +_, o, _ = c.exec_command('docker exec njts-accounting-backend ls -la /app/frontend/ | head -20') +print("=== コンテナ内 /app/frontend ===") +print(o.read().decode()) +# コンテナのマウント情報 +_, o2, _ = c.exec_command('docker inspect njts-accounting-backend --format "{{json .Mounts}}"') +import json +mounts = json.loads(o2.read().decode()) +for m in mounts: + print(f" {m.get('Source')} -> {m.get('Destination')}") +c.close() diff --git a/_check_nas_index.py b/_check_nas_index.py new file mode 100644 index 0000000..bea733e --- /dev/null +++ b/_check_nas_index.py @@ -0,0 +1,10 @@ +import paramiko +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect('192.168.0.61', username='root', password='59911784', timeout=15) +_, o, _ = c.exec_command('grep -n "bank-statement" /volume1/docker/njts-accounting/frontend/index.html') +result = o.read().decode() +print("NAS index.html grep結果:", repr(result)) +_, o2, _ = c.exec_command('wc -l /volume1/docker/njts-accounting/frontend/index.html') +print("行数:", o2.read().decode().strip()) +c.close() diff --git a/_check_nas_index2.py b/_check_nas_index2.py new file mode 100644 index 0000000..1932bd8 --- /dev/null +++ b/_check_nas_index2.py @@ -0,0 +1,7 @@ +import paramiko +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect('192.168.0.61', username='root', password='59911784', timeout=15) +_, o, _ = c.exec_command('sed -n "187,205p" /volume1/docker/njts-accounting/frontend/index.html') +print(o.read().decode()) +c.close() diff --git a/backend/app/db_auto_migration.py b/backend/app/db_auto_migration.py index 92741d8..1130d78 100644 --- a/backend/app/db_auto_migration.py +++ b/backend/app/db_auto_migration.py @@ -146,6 +146,13 @@ def run_auto_migration(): except Exception as e: print(f"⚠️ ねんきんポータル テーブル作成エラー(無視): {e}") + # ── 銀行明細テーブル作成 ── + try: + from app.modules.bank_statements.migration import create_bank_statement_table + create_bank_statement_table() + except Exception as e: + print(f"⚠️ 銀行明細テーブル作成エラー(無視): {e}") + except Exception as e: print(f"\n❌ 無法連接到數據庫或執行遷移: {e}") print("\n提示:") diff --git a/backend/app/main.py b/backend/app/main.py index e848bfe..94e789c 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -126,6 +126,9 @@ app.include_router(file_upload.router) from app.modules.egov.router import router as egov_router app.include_router(egov_router) +from app.modules.bank_statements.router import router as bank_statements_router +app.include_router(bank_statements_router) + # ───────────────────────── # 給与モジュール (Payroll Module) # ───────────────────────── diff --git a/backend/app/modules/bank_statements/__init__.py b/backend/app/modules/bank_statements/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/modules/bank_statements/migration.py b/backend/app/modules/bank_statements/migration.py new file mode 100644 index 0000000..a79268c --- /dev/null +++ b/backend/app/modules/bank_statements/migration.py @@ -0,0 +1,41 @@ +"""銀行明細テーブルのマイグレーション""" +import os +from psycopg import connect + + +def create_bank_statement_table(): + conn = connect( + host=os.getenv("DB_HOST"), + port=int(os.getenv("DB_PORT", 5432)), + dbname=os.getenv("DB_NAME"), + user=os.getenv("DB_USER"), + password=os.getenv("DB_PASSWORD"), + ) + with conn.cursor() as cur: + cur.execute(""" + CREATE TABLE IF NOT EXISTS bank_statement_rows ( + id SERIAL PRIMARY KEY, + tx_date DATE NOT NULL, + description TEXT NOT NULL DEFAULT '', + debit BIGINT NOT NULL DEFAULT 0, + credit BIGINT NOT NULL DEFAULT 0, + balance BIGINT, + memo TEXT NOT NULL DEFAULT '', + bank_name TEXT NOT NULL DEFAULT '', + import_batch_id TEXT NOT NULL, + row_hash TEXT NOT NULL, + imported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT uq_bank_stmt_hash UNIQUE (row_hash) + ) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_bank_stmt_date + ON bank_statement_rows (tx_date) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_bank_stmt_batch + ON bank_statement_rows (import_batch_id) + """) + conn.commit() + print("✓ bank_statement_rows テーブル確認完了") + conn.close() diff --git a/backend/app/modules/bank_statements/router.py b/backend/app/modules/bank_statements/router.py new file mode 100644 index 0000000..6e56d35 --- /dev/null +++ b/backend/app/modules/bank_statements/router.py @@ -0,0 +1,427 @@ +""" +銀行明細 CSV 取込・照会モジュール +""" +import csv +import hashlib +import io +import uuid +from datetime import datetime, date +from typing import Optional, List + +from fastapi import APIRouter, HTTPException, UploadFile, File, Form +from pydantic import BaseModel + +from app.core.database import get_connection + +router = APIRouter(prefix="/bank-statements", tags=["銀行明細"]) + +# ── CSV 列名検出パターン ────────────────────────────────────── + +_DATE_KEYS = ['日付', '取引日', '年月日', '取引年月日', 'date', '取引日付'] +_DESC_KEYS = ['摘要', '内容', '取引内容', '支払先', '摘要/メモ', '入出金先内容', + 'description', '取引先', 'お取引内容', '取引詳細'] +_DEBIT_KEYS = ['お支払い金額', 'お支払金額', '支払額', '引出し', '出金', '引出額', + '借方', 'debit', '出金金額', '支払い金額', '引き出し金額'] +_CREDIT_KEYS = ['お預かり金額', '預入れ', '預入', '入金', '預入金額', + '貸方', 'credit', '入金金額', 'お預り金額', '預け入れ金額'] +_BALANCE_KEYS = ['残高', '差引残高', 'balance', '帳簿残高', '取引後残高'] +_MEMO_KEYS = ['メモ', '備考', '通帳メモ', 'memo', '注記', '備考欄'] + + +def _find_col(headers: List[str], patterns: List[str]) -> Optional[int]: + lowers = [h.strip().lower() for h in headers] + for pat in patterns: + p = pat.lower() + for i, h in enumerate(lowers): + if p in h: + return i + return None + + +def _parse_amount(s: str) -> int: + if not s: + return 0 + cleaned = (s.strip() + .replace(',', '').replace(',', '') + .replace('¥', '').replace('¥', '') + .replace(' ', '').replace('\u3000', '')) + if cleaned in ('', '-', '―', '-', '▲', 'ー'): + return 0 + try: + return int(float(cleaned)) + except ValueError: + return 0 + + +def _parse_date(s: str) -> Optional[date]: + s = (s.strip() + .replace('/', '-').replace('.', '-') + .replace('年', '-').replace('月', '-').replace('日', '')) + for fmt in ['%Y-%m-%d', '%y-%m-%d']: + try: + return datetime.strptime(s, fmt).date() + except ValueError: + continue + s2 = s.replace('-', '') + if len(s2) == 8: + try: + return datetime.strptime(s2, '%Y%m%d').date() + except ValueError: + pass + if len(s2) == 6: + try: + return datetime.strptime('20' + s2, '%Y%m%d').date() + except ValueError: + pass + return None + + +def _detect_encoding(raw: bytes) -> str: + for enc in ['utf-8-sig', 'utf-8', 'shift_jis', 'cp932', 'euc-jp']: + try: + raw.decode(enc) + return enc + except (UnicodeDecodeError, LookupError): + continue + return 'utf-8' + + +def _make_hash(tx_date: str, description: str, debit: int, + credit: int, balance: Optional[int]) -> str: + s = f"{tx_date}|{description}|{debit}|{credit}|{balance}" + return hashlib.md5(s.encode()).hexdigest() + + +# ── 三菱UFJ MEISAI フォーマット対応 ────────────────────────── +# 行タイプ: "1"=口座ヘッダー, "2"=明細, "8"=フッター, "9"=合計 +# 列: [タイプ, 日付, 摘要区分, 摘要詳細, 出金額, 入金額, 残高] + +def _is_meisai_format(rows: List[List[str]]) -> bool: + """三菱UFJ銀行 MEISAI フォーマット判定""" + type1 = any(r and r[0] == '1' for r in rows) + type2 = any(r and r[0] == '2' for r in rows) + return type1 and type2 + + +def _parse_meisai_rows(rows: List[List[str]]) -> tuple: + """MEISAI フォーマットをパース。(明細リスト, 銀行名ヒント) を返す""" + bank_hint = '' + parsed = [] + + for row in rows: + if not row: + continue + row_type = row[0] + + if row_type == '1' and len(row) >= 3: + # ヘッダー行: [1, 支店コード, 支店名, ..., 科目, 口座番号, 口座名, ...] + branch = row[2].strip() if len(row) > 2 else '' + bank_hint = f"三菱UFJ銀行 {branch}" if branch else '三菱UFJ銀行' + + elif row_type == '2' and len(row) >= 5: + # 明細行: [2, 日付, 摘要区分, 摘要詳細, 出金額, 入金額, 残高] + tx_date = _parse_date(row[1]) + if tx_date is None: + continue + + summary1 = row[2].strip() if len(row) > 2 else '' + summary2 = row[3].strip() if len(row) > 3 else '' + desc = f"{summary1} {summary2}".strip(' ') if summary2 else summary1 + + debit = _parse_amount(row[4]) if len(row) > 4 else 0 + credit = _parse_amount(row[5]) if len(row) > 5 else 0 + bal_str = row[6].strip() if len(row) > 6 else '' + balance = _parse_amount(bal_str) if bal_str else None + + parsed.append({ + 'tx_date': tx_date.isoformat(), + 'description': desc, + 'debit': debit, + 'credit': credit, + 'balance': balance, + 'memo': '', + 'row_hash': _make_hash(tx_date.isoformat(), desc, debit, credit, balance), + }) + + return parsed, bank_hint + + +def _parse_csv_bytes(content: bytes) -> tuple: + """CSVをパース。(明細リスト, 銀行名ヒント) を返す""" + enc = _detect_encoding(content) + text = content.decode(enc).replace('\x00', '').strip() + + reader = csv.reader(io.StringIO(text)) + rows = list(reader) + + if not rows: + raise HTTPException(status_code=400, detail="CSVファイルが空です") + + # MEISAI(三菱UFJ)フォーマット優先検出 + if _is_meisai_format(rows): + return _parse_meisai_rows(rows) + + # ── 汎用ヘッダー行検出 ── + header_row = 0 + for i, row in enumerate(rows[:15]): + row_str = ' '.join(row).lower() + if any(k.lower() in row_str for k in ['日付', '取引日', 'date', '年月日']): + header_row = i + break + + headers = rows[header_row] + date_col = _find_col(headers, _DATE_KEYS) + desc_col = _find_col(headers, _DESC_KEYS) + debit_col = _find_col(headers, _DEBIT_KEYS) + credit_col = _find_col(headers, _CREDIT_KEYS) + balance_col = _find_col(headers, _BALANCE_KEYS) + memo_col = _find_col(headers, _MEMO_KEYS) + + if date_col is None: + raise HTTPException( + status_code=400, + detail="日付列が見つかりません。CSVのヘッダー行に「日付」「取引日」等の列名が必要です。" + ) + + parsed = [] + for row in rows[header_row + 1:]: + if not row or all(c.strip() == '' for c in row): + continue + if len(row) <= date_col: + continue + raw_date = row[date_col].strip() + if not raw_date: + continue + tx_date = _parse_date(raw_date) + if tx_date is None: + continue + + def _safe(col: Optional[int]) -> str: + return row[col].strip() if col is not None and len(row) > col else '' + + desc = _safe(desc_col) + debit = _parse_amount(_safe(debit_col)) + credit = _parse_amount(_safe(credit_col)) + bal_str = _safe(balance_col) + balance = _parse_amount(bal_str) if bal_str else None + memo = _safe(memo_col) + + parsed.append({ + 'tx_date': tx_date.isoformat(), + 'description': desc, + 'debit': debit, + 'credit': credit, + 'balance': balance, + 'memo': memo, + 'row_hash': _make_hash(tx_date.isoformat(), desc, debit, credit, balance), + }) + + return parsed, '' + + +# ── エンドポイント ───────────────────────────────────────────── + +@router.post("/preview", summary="CSVプレビュー(重複チェック)") +async def preview_csv( + file: UploadFile = File(...), + bank_name: str = Form(""), +): + """CSVを解析し、重複チェック結果付きでプレビューを返す(DBには保存しない)""" + content = await file.read() + if len(content) > 10_000_000: + raise HTTPException(status_code=400, detail="ファイルサイズが大きすぎます(上限10MB)") + + rows, bank_hint = _parse_csv_bytes(content) + if not rows: + raise HTTPException(status_code=400, detail="有効なデータ行がありません") + + # 銀行名未入力の場合はCSVから自動検出した値を使用 + if not bank_name.strip() and bank_hint: + bank_name = bank_hint + + # DB上の既存ハッシュを取得して重複チェック + with get_connection() as conn, conn.cursor() as cur: + cur.execute("SELECT row_hash FROM bank_statement_rows") + existing_hashes = {r['row_hash'] for r in cur.fetchall()} + + result = [] + for row in rows: + row['status'] = 'duplicate' if row['row_hash'] in existing_hashes else 'new' + result.append(row) + + new_count = sum(1 for r in result if r['status'] == 'new') + dup_count = sum(1 for r in result if r['status'] == 'duplicate') + + return { + 'total': len(result), + 'new': new_count, + 'duplicates': dup_count, + 'bank_name': bank_name, + 'rows': result, + } + + +class ImportRow(BaseModel): + tx_date: str + description: str + debit: int + credit: int + balance: Optional[int] = None + memo: str = '' + row_hash: str = '' + + +class ImportRequest(BaseModel): + bank_name: str = '' + rows: List[ImportRow] + + +@router.post("/import", summary="CSV取込確定") +def import_rows(req: ImportRequest): + """プレビュー確認後、新規行のみDBに取込む""" + if not req.rows: + raise HTTPException(status_code=400, detail="取込データがありません") + + batch_id = str(uuid.uuid4()) + inserted = 0 + skipped = 0 + + with get_connection() as conn, conn.cursor() as cur: + for row in req.rows: + # row_hash が空の場合は再計算 + rh = row.row_hash or _make_hash( + row.tx_date, row.description, row.debit, row.credit, row.balance + ) + cur.execute(""" + INSERT INTO bank_statement_rows + (tx_date, description, debit, credit, balance, + memo, bank_name, import_batch_id, row_hash) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (row_hash) DO NOTHING + """, ( + row.tx_date, row.description, + row.debit, row.credit, row.balance, + row.memo, req.bank_name, batch_id, rh, + )) + if cur.rowcount > 0: + inserted += 1 + else: + skipped += 1 + conn.commit() + + return { + 'batch_id': batch_id, + 'inserted': inserted, + 'skipped': skipped, + } + + +@router.get("", summary="明細一覧取得") +def list_statements( + year: Optional[int] = None, + month: Optional[int] = None, + bank_name: Optional[str] = None, + limit: int = 10000, + offset: int = 0, +): + conditions: list = [] + params: list = [] + + if year: + conditions.append("EXTRACT(YEAR FROM tx_date)::int = %s") + params.append(year) + if month: + conditions.append("EXTRACT(MONTH FROM tx_date)::int = %s") + params.append(month) + if bank_name: + conditions.append("bank_name = %s") + params.append(bank_name) + + where_clause = ("WHERE " + " AND ".join(conditions)) if conditions else "" + + with get_connection() as conn, conn.cursor() as cur: + # 明細行 + cur.execute(f""" + SELECT id, + TO_CHAR(tx_date, 'YYYY-MM-DD') AS tx_date, + description, + debit::bigint, + credit::bigint, + balance::bigint, + memo, + bank_name, + import_batch_id + FROM bank_statement_rows + {where_clause} + ORDER BY tx_date, id + LIMIT %s OFFSET %s + """, params + [limit, offset]) + rows = cur.fetchall() + + # 月別集計 + cur.execute(f""" + SELECT + TO_CHAR(tx_date, 'YYYY-MM') AS ym, + SUM(debit)::bigint AS total_debit, + SUM(credit)::bigint AS total_credit, + COUNT(*)::int AS count + FROM bank_statement_rows + {where_clause} + GROUP BY ym + ORDER BY ym + """, params) + monthly = cur.fetchall() + + # 総件数 + cur.execute( + f"SELECT COUNT(*)::int AS cnt FROM bank_statement_rows {where_clause}", + params + ) + total = cur.fetchone()['cnt'] + + # 銀行名リスト + cur.execute(""" + SELECT DISTINCT bank_name + FROM bank_statement_rows + WHERE bank_name != '' + ORDER BY bank_name + """) + banks = [r['bank_name'] for r in cur.fetchall()] + + return { + 'total': total, + 'rows': rows, + 'monthly': monthly, + 'banks': banks, + } + + +@router.get("/batches", summary="取込バッチ一覧") +def list_batches(): + with get_connection() as conn, conn.cursor() as cur: + cur.execute(""" + SELECT + import_batch_id, + bank_name, + MIN(tx_date)::text AS date_from, + MAX(tx_date)::text AS date_to, + COUNT(*)::int AS count, + MIN(imported_at)::text AS imported_at + FROM bank_statement_rows + GROUP BY import_batch_id, bank_name + ORDER BY MIN(imported_at) DESC + LIMIT 100 + """) + return cur.fetchall() + + +@router.delete("/batch/{batch_id}", summary="バッチ削除") +def delete_batch(batch_id: str): + with get_connection() as conn, conn.cursor() as cur: + cur.execute( + "DELETE FROM bank_statement_rows WHERE import_batch_id = %s", + (batch_id,) + ) + deleted = cur.rowcount + conn.commit() + return {'deleted': deleted} diff --git a/create_bank_table.py b/create_bank_table.py new file mode 100644 index 0000000..c4c7556 --- /dev/null +++ b/create_bank_table.py @@ -0,0 +1,63 @@ +"""銀行明細テーブルを直接作成する""" +import paramiko + +HOST = "192.168.0.61" +USER = "root" +PASS = "59911784" + +SQL = """ +CREATE TABLE IF NOT EXISTS bank_statement_rows ( + id SERIAL PRIMARY KEY, + tx_date DATE NOT NULL, + description TEXT NOT NULL DEFAULT '', + debit BIGINT NOT NULL DEFAULT 0, + credit BIGINT NOT NULL DEFAULT 0, + balance BIGINT, + memo TEXT NOT NULL DEFAULT '', + bank_name TEXT NOT NULL DEFAULT '', + import_batch_id TEXT NOT NULL, + row_hash TEXT NOT NULL, + imported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT uq_bank_stmt_hash UNIQUE (row_hash) +); +CREATE INDEX IF NOT EXISTS idx_bank_stmt_date ON bank_statement_rows (tx_date); +CREATE INDEX IF NOT EXISTS idx_bank_stmt_batch ON bank_statement_rows (import_batch_id); +""" + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect(HOST, username=USER, password=PASS, timeout=15) + +cmd = f"""docker exec njts-accounting-backend python -c " +import os, psycopg +conn = psycopg.connect(host=os.getenv('DB_HOST'), port=os.getenv('DB_PORT'), dbname=os.getenv('DB_NAME'), user=os.getenv('DB_USER'), password=os.getenv('DB_PASSWORD')) +cur = conn.cursor() +cur.execute(\\\"\\\"\\\" +CREATE TABLE IF NOT EXISTS bank_statement_rows ( + id SERIAL PRIMARY KEY, + tx_date DATE NOT NULL, + description TEXT NOT NULL DEFAULT '', + debit BIGINT NOT NULL DEFAULT 0, + credit BIGINT NOT NULL DEFAULT 0, + balance BIGINT, + memo TEXT NOT NULL DEFAULT '', + bank_name TEXT NOT NULL DEFAULT '', + import_batch_id TEXT NOT NULL, + row_hash TEXT NOT NULL, + imported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT uq_bank_stmt_hash UNIQUE (row_hash) +) +\\\"\\\"\\\") +cur.execute('CREATE INDEX IF NOT EXISTS idx_bank_stmt_date ON bank_statement_rows (tx_date)') +cur.execute('CREATE INDEX IF NOT EXISTS idx_bank_stmt_batch ON bank_statement_rows (import_batch_id)') +conn.commit() +print('テーブル作成完了') +conn.close() +" +""" + +_, o, e = client.exec_command(cmd, timeout=30) +print(o.read().decode()) +err = e.read().decode() +if err: print("STDERR:", err) +client.close() diff --git a/docs/MEISAI20260531234440.csv b/docs/MEISAI20260531234440.csv new file mode 100644 index 0000000..dd87387 --- /dev/null +++ b/docs/MEISAI20260531234440.csv @@ -0,0 +1,32 @@ +"1","082","юxX","1","0","","0788058","V{eNm\[VY@","2026.4.1-2026.5.31","S","2026.5.31","23:44","","","" +"2","2026.4.15","UP","Jjj|\|VY","0","630000","17158067" +"2","2026.4.20","UւR","`E^CLEJPL","41000","0","17117067" +"2","2026.4.28","UւR","yg[CVzEVE","11000","0","17106067" +"2","2026.4.30","Љی","","247066","0","16859001" +"2","2026.4.30","UP","XYV|giJ","0","1606000","18465001" +"2","2026.4.30","UayQ","JjGtP|GX","170000","0","18295001" +"2","2026.4.30","ב֎萔","tR~@eXEE","660","0","18294341" +"2","2026.4.30","UayP","IE@JN","353499","0","17940842" +"2","2026.4.30","ב֎萔","tR~@eXEE","330","0","17940512" +"2","2026.4.30","UayP","`E@VEJN","4026825","0","13913687" +"2","2026.4.30","ב֎萔","tR~@eXEE","330","0","13913357" +"2","2026.5.1","J|h","","1000000","0","12913357" +"2","2026.5.7","J|h","","1000000","0","11913357" +"2","2026.5.15","UP","Jjj|\|VY","0","630000","12543357" +"2","2026.5.18","UւR","`E^CLEJPL","41000","0","12502357" +"2","2026.5.18","UP","G^c@iJ","0","4620000","17122357" +"2","2026.5.20","UayP","IE@JN","2787776","0","14334581" +"2","2026.5.20","ב֎萔","tR~@eXEE","330","0","14334251" +"2","2026.5.28","UւR","yg[CVzEVE","11000","0","14323251" +"2","2026.5.29","UayQ","JjGtP|GX","170000","0","14153251" +"2","2026.5.29","ב֎萔","tR~@eXEE","660","0","14152591" +"2","2026.5.29","UayQ","`EVEJN","350781","0","13801810" +"2","2026.5.29","ב֎萔","tR~@eXEE","660","0","13801150" +"2","2026.5.29","UayP","IE@JN","353091","0","13448059" +"2","2026.5.29","ב֎萔","tR~@eXEE","330","0","13447729" +"2","2026.5.29","UayQ","G^c@iJ","824340","0","12623389" +"2","2026.5.29","ב֎萔","tR~@eXEE","660","0","12622729" +"2","2026.5.29","UP","XYV|giJ","0","1694000","14316729" +"2","2026.5.29","UP","G^c@iJ","0","825000","15141729" +"8" +"9","23","6","","11391338","10005000","16528067","15141729" diff --git a/docs/三菱UFJ_APIのアカウント資料.xlsx b/docs/三菱UFJ_APIのアカウント資料.xlsx new file mode 100644 index 0000000..2095f3f Binary files /dev/null and b/docs/三菱UFJ_APIのアカウント資料.xlsx differ diff --git a/frontend/bank-statement.html b/frontend/bank-statement.html new file mode 100644 index 0000000..470ca86 --- /dev/null +++ b/frontend/bank-statement.html @@ -0,0 +1,768 @@ + + +
+ + + ++ 銀行のCSVデータを取込み、明細を管理します。重複チェック付きで安全に取込め、月別・銀行別の検索・印刷が可能です。 +
++ 銀行明細を開く +
+@@ -263,18 +275,6 @@ } } - // dropdown toggle - const toggle = document.getElementById("accountingToggle"); - const menu = document.getElementById("accountingMenu"); - toggle.addEventListener("click", (e) => { - menu.style.display = menu.style.display === "block" ? "none" : "block"; - }); - // click outside to close - document.addEventListener("click", (e) => { - if (!toggle.contains(e.target) && !menu.contains(e.target)) { - menu.style.display = "none"; - } - }); diff --git a/frontend/journal-entry.html b/frontend/journal-entry.html index 7b3921a..8d2f533 100644 --- a/frontend/journal-entry.html +++ b/frontend/journal-entry.html @@ -257,22 +257,7 @@
- + ← メニューへ戻る