CodexでレシートPDFからBeancountへの仕訳登録を自動化した

CodexでレシートPDFを読み取り、既存科目とファイル名の支払方法からBeancountの仕訳を作る運用を実装した。1取引ずつbean-checkを実行し、残高照合で未記帳のチャージも見つけた。

PDFで保存したレシートを、Beancountの帳簿へ転記する作業をCodexに任せることにした。既存の勘定科目を使って仕訳案を作り、人間が確認したら帳簿へ書き込む。登録するたびにbean-checkを実行し、成功するまで次の取引へ進まない運用にした。

自動化したのは読取り、仕訳案の作成、証跡の整理、帳簿への転記と検証である。仕訳案の最終確認は人間が行う。ファイル監視で無人処理する仕組みにはしていない。

全体の流れは次のとおりである。

  1. 人間がレシートをPDFで保存し、ファイル名に支払方法を付ける。
  2. Codexが全ページを読み、既存科目と支払方法から仕訳案を作る。
  3. 人間が仕訳案と証跡の組合せを確認する。
  4. 補助ツールが証跡を保存し、1取引ずつ帳簿へ登録する。
  5. 各登録の直後にbean-checkを実行し、成功したら次へ進む。

Codexが判断し、Pythonが書き込む

帳簿は次の構成で管理している。

ファイル役割
account.beancount勘定科目と日本語の名称
year/2026.beancount2026年の仕訳
main.beancount科目、各年の仕訳などを読み込む入口

Codexには、レシートの内容に加えてaccount.beancountと既存の仕訳を読ませる。店舗名だけで費用科目を決めず、酒類、書籍、レジ袋などを品目に応じて分ける。消費税も既存の記帳方法に合わせて別科目へ計上する。

読取りと判断はCodex、金額の検証とファイル操作はPythonの補助ツールに分担した。補助ツール自体にはOCRやAI APIの呼出しを組み込んでいない。

実装環境はPython 3、Beancount 3.2.0、Popplerである。手順はリポジトリのAGENTS.mdに記載し、補助ツールをscripts/receipt.pyとして作成した。

抽出テキストだけで転記しない

スキャンしたPDFには抽出可能なテキストが入っていても、数字や店舗名に誤読があった。そこで、pdftotextで抽出した内容を、pdftoppmで画像化した全ページと照合する。取引日、合計、税額、割引は画像でも確認する。

実際に、ファイル名の日付と取引日が違うレシートがあった。ファイル名が2026-10-11で始まるPDFに、10月8日の給油が記載されていた。別のPDFも、ファイル名は2026-10-11で始まるが取引日は10月9日だった。

帳簿の日付にはレシートの取引日を使う。読めない数字や証跡間の矛盾があれば、推測して登録せず人間に質問させる。

支払方法はファイル名に書く

最初は、レシートを読むたびに支払方法を質問させていた。カード払いと分かっても、どのカードかは帳簿の科目へ対応させる必要があるためだ。

毎回回答する手間を減らすため、自分でPDF名の末尾に支払方法を付けることにした。次のような形式である。

日付.店舗名.支払方法.pdf

次の表は対応の一例で、今回使った支払方法を示している。

ファイル名の支払方法支払科目
現金Assets:Cash:Purse
モバイルSuicaAssets:ICCard:MobileSuica
ビックカメラVIEWLiabilities:CreditCard:BiccameraVIEWSuicaCard

補助ツールは.pdfの直前の要素を対応表で変換する。店舗名の一部から支払方法を推測することはない。

ファイル名から判別できれば、支払方法の質問は省略する。未記載、未知の表記、PDFの内容との明確な不一致がある場合は質問する。仕訳案の最終確認は、支払方法が分かっていても省略しない。

PDFの数と取引の数を分ける

領収書とカードの利用票は、同じ購入を示す別の証跡である。これらを別取引として登録すると、費用を二重計上してしまう。

同じPDFの再取込みは、ファイルごとのSHA-256で検出する。ハッシュをdocumentのメタデータへ保存し、取込み済みの証跡と照合する。別のPDFが同じ購入を示している場合は、取引情報でも確認する。

注文番号や伝票番号を優先し、店舗、日付、金額も使って同じ取引の証跡をまとめる。複数PDFの組合せはCodexが候補を提示し、人間が確認する。1つのPDFに領収書と利用票の2ページがある場合も、1取引として扱う。

仕訳と証跡はBeancountのリンクで紐づける。次は今回登録した給油の例で、店舗名とPDF名は説明用に置き換えている。

2026-10-08 * "ガソリンスタンド" "ハイオク給油" ^receipt-example
  Expenses:Car:Gasoline                         2696 JPY
  Expenses:Tax:Consumption-Tax                   270 JPY
  Liabilities:CreditCard:BiccameraVIEWSuicaCard  -2966 JPY

2026-10-08 document Expenses:Car:Gasoline "../Expenses/Car/Gasoline/receipt.pdf" ^receipt-example

既存取引と日付、店舗、費用総額が一致した場合も確認を求める。後から別の証跡を受け取った場合は、既存取引に証跡だけを追加できる。

PDFは費用科目に応じたフォルダへ移動する。複数科目がある場合は、税を除いて費用額が最大の科目を保管先にする。複数取引で共有するPDFは1回だけ保存し、関連する全取引の登録が終わるまで元ファイルを残す。

1取引ずつbean-checkを通す

補助ツールは、Codexが作ったJSON形式の仕訳案を受け取り、次の順序で処理する。

  1. 既存の帳簿にエラーがないことを確認する。
  2. 今回の1取引を一時帳簿で検証する。
  3. PDFを保管先へコピーし、ハッシュを確認する。
  4. その取引だけを本帳簿へ書き込む。
  5. bean-check main.beancountを実行する。
  6. 成功したら処理済みとし、不要になった元PDFを削除する。

通常のコピー、書込み、検証に失敗した場合は、今回の帳簿変更を戻して元PDFを残す。失敗した取引を飛ばして後続の処理を進めない。

専用ツールのcheckは事前検証、commitは1取引の登録を行う。ここでのcommitはGitのコミットではない。

python3 scripts/receipt.py check proposal.json --transaction receipt-example
python3 scripts/receipt.py commit proposal.json --transaction receipt-example

今回作成したツール専用のコマンドなので、Beancountをインストールするだけでは使えない。この記事は実装と運用の記録であり、ツールの配布手順は扱っていない。

最初にPDFを1件登録し、続いて5件を登録した。後者では領収書と利用票を含む全8ページを確認し、各取引の登録直後に検証が成功した。証跡の移動後はハッシュの一致も確認できた。

補助ツールの13件のテストも通った。対象には、複数証跡、重複候補、未確認の仕訳、ファイル名の支払方法、コピー失敗、検証失敗後の停止と再開が含まれる。

残高照合で未記帳のチャージを見つけた

レシートのない鉄道、飲料、Suicaチャージも、日付、金額、支払方法を伝えて登録した。その後、自分が確認したモバイルSuicaの実残高を帳簿に追記した。

2026-10-11 balance Assets:ICCard:MobileSuica 11362 JPY

この時点でbean-checkは失敗した。識別に必要なメッセージを抜粋すると、次の内容だった。

Balance failed for 'Assets:ICCard:MobileSuica': expected 11362 JPY != accumulated 1362 JPY (10000 too little)

帳簿が実残高より10,000円少なかった。自分が10月5日のチャージを思い出して伝えると、Codexがその日の帳簿にチャージがないことを確認した。ビックカメラVIEW Suicaカードからの10,000円チャージを追加したところ、残高照合を含めて検証が成功した。

仕訳の検証が通っていても、取引の漏れまでは見つからない。実残高をbalanceで照合したことで、未記帳を発見できた。

現状の対象は2026年の日本円による通常の購入で、外貨や返品は扱っていない。また、突然のプロセス終了や電源断に対して、帳簿とPDFの更新を完全に一体化する保証はない。今回確認できたのは、通常の失敗時の巻き戻しと、対話で確認した仕訳を順番に登録する運用である。

Licensed under CC BY-NC-SA 4.0
Hugo で構築されています。
テーマ Stack は Jimmy によって設計されています。