tax-assistant: 仕訳ルール自動推測・提案機能
クレカ明細のうち、既存の仕訳ルールに一致しなかった「未マッチ明細」を分析して、AIが勘定科目・税区分を推測し、仕訳ルールの登録を手伝う機能を新しく作った。明細を1件ずつ手動分類する作業をできるだけ減らしたかったのが動機。
全体の構成
バックエンド(FastAPI + SQLite)に未マッチ明細の集計・保存APIを追加し、フロントエンド(Vue.js)にMiller Column Layoutの専用画面を作った。AIによる推測結果は /suggest-rules コマンドで一括生成し、DBに保存する。ユーザーはUI上で推測結果を確認・修正してから仕訳ルールとして登録する流れになっている。
バックエンド: rule_suggestionsテーブルとAPI
テーブル設計
マイグレーション014で rule_suggestions テーブルを新設した。未マッチ明細をグルーピングした結果とAIの推測内容を保持する。ステータス管理(pending / approved / rejected)も持たせて、承認フローを回せるようにした。
APIエンドポイント
4つのエンドポイントを追加した。
| エンドポイント | 用途 |
|---|---|
| unmatched-groups取得 | 摘要でグルーピングした未マッチ明細一覧 |
| 個別明細取得 | 特定グループの明細詳細 |
| 一括保存 | AI推測結果のDB保存 |
| ステータス更新 | 承認・却下の反映 |
摘要でグルーピングする際に document_types とLEFT JOINして帳票情報(カード明細、銀行明細など)も一緒に取れるようにした。どの帳票由来の明細かが分かると、勘定科目の推測精度の判断材料になる。
フロントエンド: RuleSuggestionView.vue
Miller Column Layoutの採用
今回のUIで一番こだわったのがMiller Column Layout。macOSのFinderのカラム表示と同じ考え方で、左から右へ階層的に絞り込んでいく。
最初は2カラムで始めたが、使いながら足りない情報を足していった結果、最終的に6カラム構成になった。
| カラム | 内容 |
|---|---|
| 1 | 帳票種別(カード明細、銀行明細...) |
| 2 | 帳票名(楽天カード、某銀行...) |
| 3 | AI提案勘定科目 |
| 4 | 税区分 |
| 5 | 補助科目 |
| 6 | 詳細テーブル(個別明細) |
段階的に進化させた経緯:
- 2カラム版: 帳票種別 → 詳細テーブル。情報が少なすぎて使いづらい
- 4カラム版: 帳票種別 → 帳票 → AI提案科目 → 詳細テーブル。だいぶ見やすくなった
- 5カラム版: 税区分カラムを追加。同じ勘定科目でも税区分が違うケースに対応
- 6カラム版: 補助科目カラムを追加。「旅費交通費」の中の「電車代」「タクシー代」を分けたいケース
Miller Column Layoutは階層的なデータの絞り込みにとても合っていた。各カラムで選択するたびに右隣のカラムが更新されるので、大量の明細でも迷わず目的のグループにたどり着ける。
キーボードナビゲーション
マウスだけだと数百件の明細を処理するのがつらいので、キーボード操作を入れた。
- 左右矢印キー: 明細間の移動
- 上下矢印キー: カラム内の項目選択
- Ctrl+Enter: 仕訳ルール登録の確定
特にCtrl+Enterでの登録確定は、「確認して即登録」のリズムが作れるので作業速度がかなり上がった。
/suggest-rulesコマンドの実行
AI推測の実行結果について。
処理内容
142件のユニーク摘要を対象に分析した。既存の仕訳ルールの傾向(どういう摘要にどの勘定科目を割り当てているか)と勘定科目マスターを参照しながら、各摘要に対して勘定科目と税区分を推測した。
事業主貸の扱い
64件は「事業主貸」(= 事業と関係ない個人的な支出)と推測された。ただし事業主貸かどうかは本人にしか判断できないケースが多い。例えば同じ飲食店でも接待なのか個人の食事なのかは文脈次第。
そこで事業主貸と推測した64件はGoogleスプレッドシートを自動生成して、顧客のSさんに確認を依頼した。AIの推測をそのまま登録するのではなく、顧客確認のステップを挟むことで間違った仕訳ルールが大量に登録されるリスクを抑えている。
マッチタイプと近似度の選択UI
仕訳ルールの「摘要マッチ」にはいくつかの方式がある。詳細テーブルのマッチ列をドロップダウンにして、4種類から選べるようにした。
| マッチタイプ | 説明 |
|---|---|
| 部分一致 | 摘要に指定文字列が含まれるか |
| 完全一致 | 摘要が指定文字列と完全に一致するか |
| レーベンシュタイン距離 | 編集距離ベースの類似度 |
| 類似度 | 別アルゴリズムでの類似度判定 |
レーベンシュタイン距離を選んだ場合は、しきい値(90%、80%、70%)を追加で選べる。しきい値が関係ないマッチタイプ(部分一致・完全一致)のときは、しきい値列がグレーアウトして選択不可になる。条件付きの入力制御はちょっとした工夫だけど、誤操作が減るので入れてよかった。
グループ編集ビューの補助科目バグ修正
開発途中で補助科目の表示バグを見つけた。
症状
あるグループに複数の補助科目が紐づいているはずなのに、UI上では最初の1件しか表示されない。
原因
配列から条件に合う要素を探す処理で .find() を使っていた。.find() は条件に一致する最初の1件だけを返すので、2件目以降が無視されていた。
修正
.find() を .filter() に変更。条件に一致する全件を返すようにした。
これは地味だけどよくやるミス。「一致するのは1件だけ」と思い込んでいると .find() を使いがちだが、複数一致がありうる場面では .filter() を使うべき。
テスト
フロントエンド: 27テスト
computeXxx 系の関数群を対象にした単体テスト。Miller Column Layoutの各カラムの計算ロジック(フィルタリング、グルーピング、集計)が正しく動くことを確認した。
主なテスト対象:
computeDocumentTypes: 帳票種別の集計computeDocuments: 帳票の絞り込みcomputeSuggestedAccounts: AI推測勘定科目の集計computeTaxCategories: 税区分の集計computeSubAccounts: 補助科目の集計computeDetailRows: 詳細テーブルの行生成
ロジックをVueコンポーネントから切り出して純粋関数にしたので、テストが書きやすかった。UIのテストはせず、データ変換ロジックだけをテストしている。
バックエンド: 11テスト
API CRUD操作とDBマイグレーションのテスト。
- unmatched-groupsエンドポイントのレスポンス形式
- 個別明細取得のフィルタリング
- 一括保存のトランザクション整合性
- ステータス更新の状態遷移
- マイグレーション014の適用・ロールバック
学んだこと
Miller Column Layoutは階層データに強い
カテゴリ → サブカテゴリ → 詳細のような階層構造を持つデータの絞り込みにはMiller Column Layoutが合う。ツリービューと比べて、各階層の全項目が常に見えているのがいい。カラムを増やしすぎると横スクロールが必要になるが、6カラムくらいまでなら24インチモニターに収まった。
AIの推測結果は顧客確認を挟む
AIが「これは事業主貸でしょう」と言っても、本当にそうかは本人にしか分からない。推測結果をスプレッドシートにまとめて顧客に確認してもらうフローにしたことで、間違った仕訳ルールの登録を防げた。少し手間は増えるが、仕訳の正確性のほうが大事。
.find()と.filter()の使い分け
配列検索で「一致するのは1件だろう」と思い込んで .find() を使うと、複数一致のケースで2件目以降を取りこぼす。データの特性をちゃんと確認してから使い分けること。今回のバグは補助科目という比較的目立つ箇所で出たから早めに気づけたが、集計処理の内部で起きていたら発見が遅れたかもしれない。
現在の状態
- rule_suggestionsテーブルとAPI 4エンドポイントが稼働中
- 6カラムMiller Column Layoutで未マッチ明細の確認・仕訳ルール登録が可能
- 142件のユニーク摘要に対するAI推測が完了
- 事業主貸64件は顧客Sさんに確認依頼済み
- フロントエンド27 + バックエンド11 = 計38テストがパス
次は、顧客Sさんからの確認結果を反映して仕訳ルールを確定させていく作業になる。