• #tax-assistant
  • #Vue.js
  • #Python
  • #FastAPI
  • #クレジットカード
  • #経費管理
開発tax-assistantメモ

クレジットカード明細インポートとレシート突き合わせ機能の実装

Square明細の突き合わせ機能に続いて、クレジットカード明細のインポートとレシート突き合わせ機能を実装した。経費計上漏れを検出する目的で、クレカ明細とスキャン済みレシートを日付・金額ベースでマッチングする。

背景と目的

解決したい課題

  1. 経費計上漏れの検出: クレカで支払ったのにレシートをスキャンし忘れているケース
  2. 私的利用の除外: 事業経費ではないクレカ利用を明示的にマーク
  3. 返金処理の追跡: キャンセル・返品による返金を適切に処理

Square明細との違い

項目Square明細クレカ明細
対象売上(入金)経費(支出)
一致判定Square ≤ 売上伝票クレカ = レシート
私的利用なしあり(privateステータス)
返金なしあり(refundステータス)

実装内容

Phase 1: データベース設計

creditcard_transactionsテーブルを追加。Square明細と同様の構造だが、私的利用(private)と返金(refund)のステータスを追加。

CREATE TABLE IF NOT EXISTS creditcard_transactions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    tx_date TEXT NOT NULL,
    amount INTEGER NOT NULL,
    description TEXT,
    category TEXT,
    source_file TEXT NOT NULL,
    row_index INTEGER NOT NULL,
    check_status TEXT DEFAULT 'unchecked',
    check_message TEXT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(source_file, row_index)
);

CREATE INDEX idx_creditcard_tx_date ON creditcard_transactions(tx_date);
CREATE INDEX idx_creditcard_check_status ON creditcard_transactions(check_status);

Phase 2: インポート機能

TSV形式のクレカ明細をパースしてインポート。重複はINSERT OR IGNOREでスキップ。

# import_creditcard.py の抜粋
def parse_creditcard_tsv(file_path: str) -> list[dict]:
    """TSV形式のクレカ明細をパース"""
    transactions = []
    with open(file_path, 'r', encoding='utf-8') as f:
        reader = csv.reader(f, delimiter='\t')
        for row_index, row in enumerate(reader):
            if len(row) < 5:
                continue
            # 金額はマイナス値で記録されている
            amount = abs(int(row[3].replace(',', '')))
            transactions.append({
                'tx_date': row[1],  # YYYY/MM/DD形式
                'amount': amount,
                'description': row[2],
                'category': row[4],
                'source_file': os.path.basename(file_path),
                'row_index': row_index
            })
    return transactions

Phase 3: 突き合わせロジック

レシートが存在する日付範囲内のクレカ明細のみをチェック対象にする。

def check_creditcard_against_receipts(db_path: str) -> dict:
    """クレカ明細とレシートの突き合わせチェック"""

    # 確定レシートの日付範囲を取得
    date_range_sql = """
        SELECT MIN(date) as min_date, MAX(date) as max_date
        FROM receipts
        WHERE confirmed = 1 AND date IS NOT NULL
    """

    # 日付範囲内のクレカ明細のみをチェック
    check_sql = """
        WITH receipt_dates AS (
            SELECT strftime('%Y-%m-%d', date) as receipt_date, amount
            FROM receipts
            WHERE confirmed = 1
        )
        UPDATE creditcard_transactions
        SET check_status = CASE
            WHEN check_status IN ('private', 'refund') THEN check_status
            WHEN EXISTS (
                SELECT 1 FROM receipt_dates
                WHERE receipt_date = strftime('%Y-%m-%d', tx_date)
                AND receipt_dates.amount = creditcard_transactions.amount
            ) THEN 'matched'
            ELSE 'unmatched'
        END
        WHERE tx_date BETWEEN ? AND ?
        AND check_status NOT IN ('private', 'refund')
    """

Phase 4: フロントエンド

CreditCardMatchingView.vueを新規作成。Square明細タブと同様のUI構造。

特徴:

  • 種別フィルター: 「支払い」「返金」
  • 年月スプリットビュー
  • サマリーバー: 一致/不一致/私的利用/返金/未チェック件数
  • チェック対象日付範囲の表示
<template>
  <div class="creditcard-matching">
    <!-- 種別カラム -->
    <div class="type-column">
      <div class="column-header">種別</div>
      <div
        v-for="type in types"
        :key="type.value"
        :class="['type-item', { active: selectedType === type.value }]"
        @click="selectType(type.value)"
      >
        {{ type.label }}
      </div>
    </div>

    <!-- 年月カラム -->
    <YearFilterColumn :years="years" ... />
    <MonthFilterColumn :months="months" ... />

    <!-- データテーブル -->
    <div class="data-table">
      <div class="summary-bar">
        全{{ total }}件 | 一致{{ matched }} 不一致{{ unmatched }}
        私的{{ private }} 返金{{ refund }} 未チェック{{ unchecked }}
        <span v-if="checkDateRange" class="date-range">
          (チェック対象: {{ checkDateRange.min }} 〜 {{ checkDateRange.max }})
        </span>
      </div>
      <!-- テーブル本体 -->
    </div>
  </div>
</template>

Codexレビューで反映した改善点

GPT-5.2(Codex)によるレビューで指摘された問題点を修正:

  1. 一意キーでの更新: date, amountではなくsource_file, row_indexで更新対象を特定
  2. 返金の自動判定: 金額がマイナスの場合は自動でrefundステータスを設定
  3. 日付範囲フィルタ: レシートが存在する日付範囲外のクレカ明細はチェック対象外
  4. 件数の一時データ化: DBには件数を保存せず、APIレスポンスで集計結果として返す

使い方

1. クレカ明細のインポート

# スラッシュコマンドでインポート
/import-creditcard

inbox/ディレクトリ内のTSVファイルを自動検出してインポート。

2. 突き合わせチェック

「クレカ明細」タブで「レシートとチェック」ボタンをクリック。

3. 不一致の確認

不一致リストを確認し、必要に応じて:

  • レシートをスキャンして追加
  • 私的利用としてマーク

今後の課題

  • 私的利用のマーク機能(UI)
  • マネーフォワード仕訳プレビューとの連携
  • 複数クレカの管理(ソース別フィルタ)