ANTIGRAVITY LABEN
記事一覧/アプリ開発
アプリ開発/2026-09-19初級

見積書 PDF の自動化を一度にまとめて頼み、4 つに割り直すまで

受託の見積書を毎回コピーして作っていた手作業を、Antigravity に頼んで小さな PDF 出力ツールへ移した記録です。最初に全部をまとめて頼んで作り直すことになった理由と、データ・計算・見た目・保存の 4 つに割って渡し直した順番を、動くコードと一緒に書き残します。

Antigravity373PDF4個人開発100自動化40Python14

前回の見積書をコピーして、宛名と金額を書き換えて送る——受託のサイト制作を引き受けるようになってから、私はずっとその作り方をしておりました。危なかったのは、コピー元の合計金額だけが前回のまま残っていた見積書を、送信の直前で見つけた夕方です。宛名も日付も品目も新しいのに、いちばん下の数字だけが古いままでした。

手が止まったのは、そのミスを自分がどうやって見つけたのか思い出せなかったからです。たまたま目が行っただけでした。次に同じ位置に目が行く保証はどこにもありません。

その週末、Antigravity に「見積書を PDF で出す小さなツールを作ってほしい」と頼みました。そして最初の一回は、ほぼ作り直しになりました。

まとめて頼んだ結果、どこが違うのか分からなくなりました

最初の依頼文は、いま読むと欲張りすぎておりました。品目を入力したら金額を計算して、会社のロゴを入れたレイアウトで組んで、PDF にして、日付入りのファイル名で保存するところまで——それを一度に書いて渡しました。

返ってきたものは、確かに動きました。動いたうえで、出てきた PDF の合計が私の電卓と 1 円ずれておりました。

困ったのは、その 1 円がどこで生まれたのか追えなかったことです。金額の計算と、表示用の書式と、PDF に流し込むときの変換が一つのファイルに同居していて、どの段で丸められたのか読み取れません。エージェントに「合計が 1 円ずれています」と伝えると、今度は別の場所に丸めを足してきて、合計は合ったのに明細の小計が合わなくなりました。

いま思えば、私は一度に渡しすぎたのではなく、確かめられる単位で渡していなかったのだと思います。一つずつ目で確かめられる大きさに切っていれば、ずれた瞬間に場所が分かったはずでした。

依頼の大きさそのものを見積もる話は エージェントへの依頼は、送信ボタンを押す前に大きさを測れる で別の角度から扱っておりますが、この日の私に必要だったのは、もっと素朴な線引きでした。

データ・計算・見た目・保存の 4 つに割る

書き直すにあたって、依頼を 4 回に分けました。分け方の基準は機能ではなく、それ単体で正しさを確かめられるかどうかです。

順番頼むこと確かめ方
1見積の中身を JSON の形に決める自分の過去の見積を 3 件書き写せるか
2金額の計算だけを関数にする手計算と突き合わせられるか
3HTML のひな形に流して PDF にする印刷プレビューで崩れていないか
4保存先とファイル名を決める同じ日に 2 通作っても上書きされないか

1 つ目はコードではありません。自分の見積書に何が載っているのかを、項目名・単価・数量・備考という粒度で書き出すだけの作業です。ここを飛ばしてエージェントに考えさせると、私の運用に無い欄が増えて、あとから削ることになります。過去 3 件を書き写してみると、私の見積には「作業に含まれないもの」を書く欄が必ずあり、そこがいちばん長いと分かりました。

この発見で JSON の形が変わりました。後回しにしていたはずの欄を配列として最初から持たせたので、ひな形の側にも置き場所ができます。先に形をエージェントへ任せていたら、きれいで一般的な形が返ってきて、私のほうが次の 3 件でそれに合わせていたはずです。

金額の計算だけを先に切り出す

2 つ目の依頼は、計算だけを受け持つファイルを作ってもらうことでした。表示も PDF も含めません。

# quote_calc.py — 金額の計算だけを受け持ちます
from decimal import Decimal, ROUND_DOWN
 
RATE = Decimal("0.10")  # 掛け率。運用に合わせてここ 1 か所だけで決めます
 
 
def line_total(unit_price: str, quantity: int) -> Decimal:
    """1 行ぶんの小計です。float ではなく文字列から Decimal を作ります。"""
    return Decimal(unit_price) * quantity
 
 
def summarize(items: list) -> dict:
    subtotal = sum((line_total(i["unit_price"], i["quantity"]) for i in items), Decimal("0"))
    surcharge = (subtotal * RATE).quantize(Decimal("1"), rounding=ROUND_DOWN)
    return {"subtotal": subtotal, "surcharge": surcharge, "total": subtotal + surcharge}
 
 
if __name__ == "__main__":
    items = [
        {"name": "トップページ デザイン", "unit_price": "120000", "quantity": 1},
        {"name": "下層ページ デザイン", "unit_price": "45000", "quantity": 3},
    ]
    print(summarize(items))
    # {'subtotal': Decimal('255000'), 'surcharge': Decimal('25500'), 'total': Decimal('280500')}

金額を float で持たないところだけは、依頼文にはっきり書きました。0.1 を浮動小数点で扱うと、桁が増えたときに一の位が揺れます。最初の版でエージェントが素直に float を使っていたのは、私が何も言わなかったからです。数字の型は、頼む側が決めて渡します。 その一行を依頼文に足してから、同じ手直しをしなくなりました。

丸める位置も、計算の段で一度だけと決めました。行ごとに丸めるか、小計に対して一度だけ丸めるかで、合計は実際にずれます。

from decimal import Decimal
 
from quote_calc import summarize
 
 
def test_surcharge_is_rounded_once_on_the_subtotal():
    items = [
        {"name": "A", "unit_price": "335", "quantity": 1},
        {"name": "B", "unit_price": "335", "quantity": 1},
        {"name": "C", "unit_price": "335", "quantity": 1},
    ]
    result = summarize(items)
    assert result["subtotal"] == Decimal("1005")
    assert result["surcharge"] == Decimal("100")  # 行ごとに丸めると 33 x 3 = 99 で 1 円ずれます
    assert result["total"] == Decimal("1105")

このテストを先に書いてもらってから本体を直すと、エージェントが丸めを別の場所へ足したときに、その場で赤くなります。最初の版で私が 1 円を追えなかったのは、赤くなる場所を用意していなかったからでした。

見た目は HTML に任せ、PDF はあとから被せる

3 つ目の依頼で初めて見た目の話に入ります。私は罫線の位置やロゴの余白を何度も直すと分かっていたので、レイアウトは HTML と CSS で組んでもらい、PDF 化はその上に薄く被せる形にしました。ブラウザで開いて直せる状態が長く残るほうが、私の直し方に合っておりました。

# render.py — 見た目だけを受け持ちます
from pathlib import Path
 
from weasyprint import HTML
 
TEMPLATE = Path("template.html").read_text(encoding="utf-8")
 
 
def render_pdf(context: dict, out_path: Path) -> None:
    html = TEMPLATE
    for key, value in context.items():
        html = html.replace("{{" + key + "}}", str(value))
    HTML(string=html, base_url=".").write_pdf(out_path)

ひな形の側では、用紙と余白と書体を先に決めておきます。

@page { size: A4; margin: 18mm; }
body { font-family: "Noto Sans JP", "Hiragino Sans", "Yu Gothic", sans-serif; }
.amount { font-variant-numeric: tabular-nums; }

数字の欄に tabular-nums を入れているのは、桁の位置を揃えるためです。等幅の数字にしておくと、金額の列が上下でぴたりと重なり、読み返したときに桁の違いに気づけます。前回の金額が残っていた見積書を見落としかけた身としては、ここは見た目の好みではなく確認のための仕掛けです。

つまずいた 2 か所

日本語が真っ白になりました。 最初に出力した PDF は、英数字だけが並んで日本語の行が空白でした。原因は書体で、CSS に書いた名前の書体がその環境に入っていなかっただけです。エージェントに聞くと CSS の書き方を直そうとしてきますが、直す場所は環境の側でした。手元に入っている書体名を調べるのは一行で済みます。Linux やコンテナなら fc-list :lang=ja family、macOS なら fc-list か Font Book です。そこに出てきた名前を CSS の先頭に置くと一度で出ます。出力する前に確かめておくほうがよいのは、真っ白な列がひな形の不具合のように見えて、見当違いのファイルを開いてしまうからです。文字が化けるのか消えるのかで原因の当たりが変わる点は、Antigravity 内蔵ターミナルで日本語が文字化けするときの原因と対処 と似ています。

改ページが明細の途中で入りました。 品目が増えた見積で、表の途中に改ページが挟まり、続きのページに見出しが無いまま数字だけが並びました。thead を使って display: table-header-group を指定すると、ページをまたいでも見出しが繰り返されます。これは PDF にして初めて見える崩れで、ブラウザの画面では最後まで気づけませんでした。品目を 30 行ほど並べた見本を 1 つ作って、毎回それで出力してから本番を作るようにしております。

保存先とファイル名を決めておく

最後の依頼が、いちばん短く済みました。

from datetime import date
 
 
def output_name(client: str, issued: date, revision: int) -> str:
    """同じ日に作り直しても上書きされない名前にします。"""
    safe = "".join(c if c.isalnum() else "_" for c in client).strip("_")
    return f"{issued:%Y%m%d}_{safe}_estimate_r{revision}.pdf"
 
 
if __name__ == "__main__":
    print(output_name("山田デザイン事務所", date(2026, 9, 19), 2))
    # 20260919_山田デザイン事務所_estimate_r2.pdf

改訂番号を名前に入れたのは、先方とのやり取りで金額が動いたときに、どちらを送ったのか後から分からなくなった経験があるからです。上書きしない決まりにしておけば、古いほうが残ります。残っていれば見比べられます。

保存先をプロジェクトの外に置いているのも、同じ理由です。最初はリポジトリの中に書き出しておりましたが、片づけの手順で必要な 1 通を消してしまいました。先方が一度目にした書類は、スクリプトが作ったものであっても成果物とは別の扱いにしております。

4 つに割ってみて分かったのは、エージェントの能力の問題ではなかったということです。私が「これで合っている」と言える単位が大きすぎただけでした。1 回目の依頼で出てきたものも、動くという意味では出来ておりました。ただ、私には確かめようがなかったのです。

いま私は、依頼を書く前に「これが返ってきたとき、何を見て合否を決めるか」を一行だけ先に書くようにしております。その一行が書けない依頼は、まだ割り足りません。

もし同じ手作業を移そうとされているなら、2 つ目——金額の計算だけを関数にして、手計算と合う 1 件のテストを書くところ——から始めてみていただければと思います。見た目より先にそこが固まっていると、あとの直しがずいぶん楽になります。

最後までお読みいただき、ありがとうございました。

シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

アプリ開発2026-05-06
Antigravity × Google AI で動かすソロ開発スタジオ — 設計から審査申請までを一人で回す自動化パイプライン
Antigravity 2.0 と Antigravity CLI を中核に、Stitch・Veo 3 を組み合わせ、アプリの設計から実装・テスト・素材生成・ストア申請までを一人で自動化するパイプラインを実装レベルで解説します。権限委譲の線引きと運用の落とし穴も扱う上級者向けの内容です。
アプリ開発2026-07-05
自己デバッグのエージェントに、本番の広告を叩かせない — AdMob 無効トラフィックを断つ三層の遮断
Antigravity 2.0 の実ブラウザ自己デバッグが本番の AdMob 広告を描画すると、無効なトラフィックとして計上されます。テスト広告の強制・ネットワーク遮断・preflight ゲートの三層で、広告に触れない検証環境を作る手順を、実測値とともにまとめました。
アプリ開発2026-07-02
審査リジェクトの朝に慌てないために — App Store の指摘をエージェントで分類し、再提出までの手順を固定する
App Store 審査リジェクトの文面を Antigravity に渡して分類し、ガイドライン別プレイブックと Info.plist リントで再提出まで最短で戻す運用設計。実測の所要時間短縮も紹介します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます