前回(新規取引先チェック)は、ほぼ「ルール+公式API」で動く話だった。今回は本物のAIが画像を読む。経費精算の「レシートと明細、ちゃんと合ってる?」を、AIに全部突合させてみた。やってみたら、人が目視で見逃すやつ——1枚のレシートを複数明細に分けた計上や、税率の混在まで拾ってきた。
たびミソは「全部AIに読ませない」こと。テキストが取れるレシートは機械が一瞬で突合、画像だけAI(Claude)が読む。速いし安い。
何をチェックする作業か
経費精算の明細は1件ごとに、添付レシートと金額・日付・税区分が合っているかを確認する。件数が多く、しかも「税込金額がレシートに本当に載っているか」を一枚ずつ見るのは地味に消耗する。ここを自動化した。
AIに任せた流れ
まずバクラクから申請と明細、レシート実体を取ってくる(ここはベンダーAPIなので擬似コード)。
# バクラクAPIで「申請中の経費精算」を取得し、明細ごとに
# 金額・日付・税区分・レシート(添付ファイル)を取り出してDLする
applications = bakuraku.fetch_requests(status="IN_PROGRESS", form="経費精算申請")
receipt_bytes = bakuraku.download(f"/workflow/user_upload_files/{file_id}/file")
レシートからテキストを抽出する。PDFならテキスト層が取れる。
import fitz # PyMuPDF
def extract_text(path: str) -> str:
"""PDF/画像からテキストを抽出する。テキスト層が無ければ空文字。"""
try:
with fitz.open(path) as doc:
return "".join(page.get_text() for page in doc)
except Exception:
return ""
そして明細の税込金額が、レシートの中に実際に出てくるかを見る。レシートは桁ごとにセル分割されたりカンマが入ったりするので、空白・カンマを全部潰してから突合する。
import re
def amount_in_text(amount: int, text: str) -> bool:
"""金額がテキストに出現するか(空白・カンマ・改行を吸収)。"""
if not amount:
return None
flat = re.sub(r"[\s,,]", "", text) # 空白とカンマを全除去
return str(abs(int(amount))) in flat
ここが今回のキモ。テキストが取れた分は上の機械処理で一瞬。取れない画像/スキャンのレシートだけ「要OCR」に振り分けて、そこをAI(Claude)が画像として読む。
if has_text_layer:
verdict = "OK" if amount_in_text(amount, text) else "要確認(金額不一致)"
else:
verdict = "要OCR(Claude)" # ← 画像はここでAIに回す
やってみて分かったこと
全部をAIに読ませる必要はない。テキストで取れるものは正規表現で一瞬・無料・確実。AI(画像OCR)は“本当に必要な画像だけ”に使う。これで速くて安くて正確になる。
やってみたら、こういうのを拾った
実際に回すと、目視だと滑りやすいものが引っかかった(いずれも内容は伏せる)。
- 分割計上:1枚のレシート(合計が大きめ)が複数の明細に割られていて、各明細の金額がレシートのどこにも出てこない→自動で「金額不一致」
- 税率の混在:明細は課税10%なのに、レシート側に軽減8%や対象外が混ざっている→「要確認」
- 適格請求書じゃない証憑:予約確認書・フリマの取引画面など、登録番号(T+13桁)が無いもの→種別を判定して注意フラグ
どこまで任せて、どこは自分がやるか
機械とAIに任せたのは「全明細を突合して、要確認を上げてくる」ところまで。承認・差し戻しは自分。
役割分担
テキスト突合=機械、画像OCR=AI、判断・差し戻し=人。“確認担当”が機械とAIに分かれただけで、最終責任は自分。
おまけ:このコードも全部AIに書かせた
前回と同じく、ここに載せたコードは自分でゴリゴリ書いていない。「レシートと明細の何を照合したいか」をAIに渡して、実装はAI、レビューと検証は自分。経費精算と支払申請で共通する処理は、AIに共通ヘルパーへ切り出させて使い回している。
まとめ
経費精算のレシート照合は「テキストは機械・画像はAI・判断は人」で全件突合。分割計上も税率混在も自動で要確認に。AIは“必要な所だけ”使うのがコツだった。