転記の検算スクリプトを書いた夜、私はまず自分の手で、ダミーの請求書 10 件のブックに間違いを 3 つ仕込みました。1 行を抜き、金額の桁を入れ替え、1 件だけ金額を文字列にしたのです。そのうえで「合計欄を目で見て気づけるか」を試したところ、気づけませんでした。ブックの合計は 276,470 円、元データは 291,650 円。差は 15,180 円ですが、行数と見た目だけ眺めていると、どこが違うのかは出てきません。
最初にお伝えしたいのは、転記を任せた相手が誰であっても、検算は「別の経路」で持つということです。Cowork が実際にこの 3 つを起こすと言いたいのではありません。人が写しても、マクロが写しても、AI が写しても起こりやすい形を、あらかじめ並べておく話です。
転記で起こりやすい 3 つのずれ方
今回の再現に使ったのは、取引先 3 社・10 件のダミーの請求書です。実在のデータは使っておりません。元データは CSV、転記結果は Excel のブックとして、次の 3 つを仕込みました。
| ずれ方 | 仕込んだ内容 | 目で見て分かりにくい理由 |
|---|---|---|
| 行の欠落 | INV-1005(5,400 円)を抜いた | 10 件が 9 件になるだけで、並びは自然 |
| 桁の入れ替え | INV-1007 を 22,750 円から 22,570 円へ | 桁数も見た目の長さも同じ |
| 金額が文字列 | INV-1003 を「9,600」という文字で入れた | 画面では数字に見えるが SUM から漏れる |
3 つ目が、いちばん手触りの悪いずれ方でした。ブックの合計欄に入れた式は =SUM(D2:D10) です。文字列として入った「9,600」は足されませんので、Excel が表示する合計は、実際に入っている値を素直に足した 286,070 円より、さらに 9,600 円少なくなります。ずれが重なって、原因を 1 つに絞れなくなるのだと、書きながら気づきました。最初のうちは、合計の差額 15,180 円を 3 つの誤りの足し算として説明しようとして、しばらく手が止まっておりました。
実際の内訳は、欠落が 5,400 円、桁の入れ替えが 180 円、文字列が 9,600 円で、合わせて 15,180 円です。足し算としては合うのですが、差額だけを見て「欠落が 1 件」とか「桁の誤りが 1 件」と当てるのは、まず無理でした。
突き合わせの考え方は「キーで引く」の一択です
検算で最初にやりたくなるのは、合計同士の比較です。ただ、合計が違うと分かっても、どの行が原因かは分かりません。逆に、合計が偶然一致してしまう間違い(片方で +100、別の行で -100)もあります。
そこで、請求書番号をキーにして、元データとブックを 1 行ずつ引き当てます。見るのは次の 4 点です。
- 元にあってブックにない行(欠落)
- ブックにあって元にない行(余分)
- 同じ番号なのに金額が違う行(不一致)
- 金額が数値ではなく文字列で入っている行(型)
件数と合計は、最後に添える参考情報という位置づけにしました。行が合っていれば合計は合います。ただ、合計が合っていても行が合うとは限りません——この向きだけは取り違えないようにしています。
検算スクリプト(そのまま動かせます)
元の CSV は invoice_no,date,vendor,amount の 4 列、ブックは同じ並びの 4 列を想定しています。合計行のように請求書番号でない行は、INV- で始まるかどうかで除外しています。
"""転記結果の検算: 元CSVと、転記で作られたブックを突き合わせる。"""
import csv, sys
from decimal import Decimal, InvalidOperation
import openpyxl
def to_amount(v):
"""数値でも "9,600" や "¥9,600" のような文字列でも Decimal にそろえる。読めなければ None。"""
if isinstance(v, (int, float)):
return Decimal(str(v))
try:
return Decimal(str(v).replace(",", "").replace("¥", "").replace("¥", "").strip())
except (InvalidOperation, AttributeError):
return None
def load_source(path):
with open(path, encoding="utf-8") as f:
return {r["invoice_no"]: Decimal(r["amount"]) for r in csv.DictReader(f)}
def load_sheet(path):
ws = openpyxl.load_workbook(path).active # 数式セルは式の文字列のまま読む
rows, raw_types = {}, {}
for r in ws.iter_rows(min_row=2, values_only=True):
no, _date, _vendor, amt = r[:4]
if not no or not str(no).startswith("INV-"): # 合計行などは除外
continue
rows[no] = to_amount(amt)
raw_types[no] = type(amt).__name__
return rows, raw_types
def main(src, xlsx):
s = load_source(src)
t, types = load_sheet(xlsx)
problems = []
for no in sorted(s.keys() - t.keys()):
problems.append(f"欠落 {no}: 元は {s[no]:,} 円なのにブックに行がありません")
for no in sorted(t.keys() - s.keys()):
problems.append(f"余分 {no}: 元データにない行です")
for no in sorted(s.keys() & t.keys()):
if t[no] is None:
problems.append(f"不正 {no}: 金額を数値として読めません")
elif t[no] != s[no]:
problems.append(f"不一致 {no}: 元 {s[no]:,} / ブック {t[no]:,}")
if types.get(no) == "str":
problems.append(f"型 {no}: 金額が文字列で入っています(SUM から漏れます)")
print(f"元の件数 {len(s)} / ブックの件数 {len(t)}")
print(f"元の合計 {sum(s.values()):,} / ブックの合計 {sum(v for v in t.values() if v is not None):,}")
for p in problems:
print(" -", p)
return 1 if problems else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1], sys.argv[2]))実行は python3 verify_transcription.py source_invoices.csv transcribed.xlsx です。仕込んだ 3 つに対して、手元では次のように出力されました。
元の件数 10 / ブックの件数 9
元の合計 291,650 / ブックの合計 286,070
- 欠落 INV-1005: 元は 5,400 円なのにブックに行がありません
- 型 INV-1003: 金額が文字列で入っています(SUM から漏れます)
- 不一致 INV-1007: 元 22,750 / ブック 22,570終了コードは、問題があれば 1、なければ 0 を返します。あとで説明する「渡し方」で、この値が効いてきます。
なぜ Decimal を使い、文字列も一度は数値に直すのか
金額を float のまま比べると、小数を含む請求書(消費税の端数など)で、見た目は同じなのに 0.1 + 0.2 のような誤差で不一致になることがあります。Decimal(str(v)) にそろえておけば、この種の誤検知を避けられます。
もう一点、文字列の「9,600」をいったん数値に直してから比べているのは、「9,600 円という中身は合っているが、入れ物が文字列である」という状態を、不一致と区別して拾いたかったからです。中身が違えば「不一致」、中身は同じで型だけが違えば「型」と、直し方の違うものを別の行で出します。型の問題は、式を入れ直す前に列の書式を数値へ戻せば済みます。
一方で、このスクリプトが見ていないものもあります。日付と取引先名は比べておりません。必要でしたら、同じ作りで date と vendor の等しさを足せます。まずは金額だけに絞ったのは、最初から全部を検査すると、出力が長くなって読まなくなるからです。
Cowork への渡し方と、私の線引き
Cowork に転記を頼むときは、このスクリプトを元データと同じフォルダに置いておき、依頼文の最後に次の一文を足しています。
転記が終わったら、同じフォルダの
verify_transcription.pyを実行して、出力をそのまま報告してください。問題が出た行は、報告のあとで直してください。
ポイントは、直す前に出力を見せてもらうところです。先に直されてしまうと、何が起きていたのかが私の側に残りません。出力があれば、欠落なのか桁の入れ替えなのかが分かり、次の依頼文の書き方(たとえば「金額の列は必ず数値で」)にも反映できます。
ただ、スクリプトを走らせたのが転記した本人(Cowork)であることには、変わりありません。重要な月は、私が自分のターミナルで同じコマンドをもう一度打ちます。写した側と、確かめる側は、できるだけ手を分けるようにしています。個人開発で外注先へ請求書の入力をお願いするときも、この線引きは変わりません。
まとめ代わりの、次の一歩
お手元にある転記済みのブックを 1 つ選び、まず元データの CSV を用意してください。そのうえで、上のスクリプトの INV- と列の並びを、ご自身のデータに合わせて 2 か所だけ書き換えてみていただければと思います。最初の一回で何も出なければ、それは本当に合っているという確認になります。何か出れば、それが次の依頼文を直すヒントになります。