• #tax-assistant
  • #OCR
  • #Gemini
  • #SQLite
  • #CSV
  • #税区分
  • #Vue
  • #日記
開発tax-assistantメモ

税務アシスタント - 新規クライアント対応と税区分機能の実装

朝イチでレシート一覧を開いたら、矢印キーで月末から翌月頭へ一気にジャンプする現象に遭遇した。「昨日まで動いていたはずなのに」とログを追い始めたところから、一日が回り始めた。

読み取り一覧のインデックス不一致バグ修正

症状

レシート一覧で矢印キーナビゲーションを使うと、月を跨いで意図しない行にカーソルが飛ぶ。ソート順も微妙にずれている。

原因

日付形式が混在していた。CSVインポート経由のデータは YYYY/MM/DD、手入力分は YYYY-MM-DD。文字列ソートでは /- のコードポイントが異なるため、同じ日付でも並び順が崩れ、インデックスと表示行がずれていた。

対処

日付正規化ヘルパーを追加し、DB格納時に YYYY-MM-DD へ統一するようにした。既存データも一括変換をかけて解消。

# 日付正規化(スラッシュ→ハイフン統一)
def normalize_date(raw: str) -> str:
    return raw.replace("/", "-").strip()

地味だが、ソートやフィルタに波及するバグだったので早めに潰せてよかった。

新規クライアント(4人目)の登録と初期設定

4人目のクライアントを追加。これまでの3人分で固まった登録フローを順に回した。

  • 勘定科目マスターCSVインポート: クライアント固有の科目体系をCSVから一括取り込み
  • 帳票設定の修正: 不要な帳票タイプを無効化。全クライアント共通のテンプレートから削って調整
  • is_activeフラグ問題: 論理削除(is_active = 0)した科目が、INSERT OR IGNOREで再インポートされると復活してしまう問題が発覚。INSERT OR IGNOREはPK重複時にスキップするだけで、既存行のis_activeを見ない。UPSERT(ON CONFLICT DO UPDATE)に切り替えて、既存行はis_activeを保持するようにした
  • スラッシュコマンドの修正: /client-add/client-configが操作先テーブルを間違えていたので修正。クライアント数が増えてきて、テーブル指定ミスが表面化した

レシートOCR処理(100件超)

複数バッチに分けて、合計100件超のレシートを処理した。

パイプライン

  1. PDF→JPEG変換: pdf_converter.pyでPDFページをJPEG化。表裏が別ページになっているレシートは1枚に連結
  2. Gemini OCR: 変換した画像をGemini APIに投げて読み取り。店名・日付・金額・明細を構造化データとして取得
  3. バリデーション: サブエージェント並列処理で検証。OCR結果と元画像のセマンティックマッチングで、金額の読み間違いや店名の文字化けを検出

100件を超えるとバッチの切り方で処理時間が変わる。今回は20〜30件ずつに分けて流し、エラーが出た分だけ手動で再処理した。

補助元帳CSV(クレカ明細)のインポート対応

クレカ明細をMF(マネーフォワード)の補助元帳CSVとしてインポートする機能を追加した。

  • MF補助元帳フォーマットのパーサー追加: ヘッダー行の位置やカラム名がMF独自仕様なので、専用パーサーを書いた
  • Shift_JIS自動検出: MFからのエクスポートはShift_JISが多い。BOM有無とバイト列パターンでエンコーディングを自動判定し、内部的にUTF-8に変換
  • 仕訳ルール自動提案(/suggest-rules: インポートしたクレカ明細の摘要欄を解析し、「この店名ならこの勘定科目」というルールを自動提案。提案を承認すると仕訳ルールDBに登録され、次回以降は自動仕訳される
  • 新帳票タイプ「仮払金明細」の追加: クレカ明細の経費精算フローに合わせて帳票タイプを新設

税区分トグルボタンの実装

仕訳出力時に税区分(課税/非課税/対象外など)を指定できるようにした。

DBマイグレーション

debit_tax_typeカラムをレシートテーブルに追加。既存の確定済みレシートにはデフォルト値を一括設定した。

ALTER TABLE receipts ADD COLUMN debit_tax_type TEXT DEFAULT '課税仕入 10%';
UPDATE receipts SET debit_tax_type = '課税仕入 10%' WHERE status = 'confirmed';

UIコンポーネント

AccountCategory.vueを汎用化した。もともと勘定科目選択専用だったが、label propを追加して「税区分」にも使えるようにした。

<!-- label propで表示名を切り替え -->
<AccountCategory label="税区分" :options="taxTypes" v-model="form.debitTaxType" />

ReceiptForm.vueに税区分UIを追加し、仕訳出力ページ(journal-viewer)でも税区分が表示されるようにした。

MF仕訳CSVエクスポートとクライアント別フィルタリング

仕訳出力ページにクライアント別の帳票フィルタリングを実装。クライアントごとに有効な帳票タイプが異なるため、選択したクライアントに応じて帳票プルダウンの選択肢を切り替える。MF仕訳CSV形式でのエクスポートも通した。

振り返り

一日で触ったレイヤーが広い。DB、パーサー、OCR、UI、エクスポート。手を動かした順に並べると「バグ修正→クライアント登録→OCRバッチ→CSVインポート→UI実装→エクスポート」で、下流から上流まで往復した格好になった。

INSERT OR IGNOREが論理削除と相性が悪い、という知見はメモしておく価値がある。PKが存在すれば何もしないので、is_active = 0で消したつもりの行が再インポートで復活しない代わりに、意図的に復活させたいときも更新されない。UPSERTに切り替えるか、インポート前に論理削除行を物理削除するか、方針を決めておく必要がある。

Shift_JISの自動検出も、今回はバイト列パターンで判定したが、chardetやcChardetを使う手もあった。ファイルサイズが小さいうちはバイト列判定で十分だが、大量ファイルを捌くならライブラリに任せたほうが安全かもしれない。