Pythonで請求書PDFを自動作成!ReportLabでインボイス対応の請求書を一括発行【フリーランス向け】
「毎月の請求書づくり、Excelのテンプレートをコピーして金額を打ち直すのが地味にしんどい…」
「インボイス制度が始まってから、請求書に何を書けばいいのか自信がない…」
フリーランスで複数の取引先を持っていると、月末の請求書作成はけっこうな手間ですよね。しかも手作業だと、転記ミスや消費税の計算ズレが起きやすいのも怖いところです。
この記事では、Python の ReportLab を使って、作業記録のCSVから取引先ごとの請求書PDFを一括で自動作成するツールを作ります。インボイス(適格請求書)に必要な項目の入れ方や、つまずきやすい「消費税の端数処理」「日本語フォント」の落とし穴も、実際に動かした結果をもとに解説します😊
- 対象読者:Pythonの基本(関数・クラス・for文)がわかる初〜中級者、請求書作成を自動化したいフリーランスの方
- 難易度:★★★☆☆(コピペで動かしながら理解できます)
- 動作確認環境:Windows 11 / Python 3.13 / ReportLab 5.0.1 / pypdf 6.19.0
CSVに作業記録を書いてコマンドを1回実行するだけで、取引先の数だけ請求書PDFが出来上がる、というところまで作ります。一緒に見ていきましょう!
全体の流れと準備

作るものは、次の4つのファイルです。
| ファイル | 役割 |
|---|---|
| invoice_calc.py | 明細と消費税の計算(税率ごとに1回だけ端数処理) |
| invoice_pdf.py | ReportLabで請求書PDFを描く |
| generate_invoices.py | CSVを読み込んで取引先ごとにPDFを一括作成 |
| check_invoices.py | できたPDFに必要項目が入っているか自動チェック |
イメージとしては、「作業記録CSV → 計算 → PDF描画 → チェック」というベルトコンベアを作る感じです。ざっくりとした流れがつかめるはずです。
まずはライブラリをインストールしておきましょう。
pip install reportlab pypdf
ReportLab はPDFを生成するライブラリ、pypdf はPDFから文字を読み取るライブラリです(最後のチェックで使います)。
インボイス(適格請求書)に必要な6項目
コードを書く前に、請求書に入れるべき項目を確認しておきます。国税庁の資料では、適格請求書には次の事項を記載することが必要とされています。
- 適格請求書発行事業者の氏名または名称と登録番号
- 取引年月日
- 取引内容(軽減税率の対象品目であればその旨)
- 税率ごとに区分して合計した対価の額(税抜または税込)と適用税率
- 税率ごとに区分した消費税額等
- 書類の交付を受ける事業者の氏名または名称
つまり、「誰が・いつ・何を・税率ごとにいくらで・消費税はいくら・誰あてに」がそろっていればOK、ということです。今回のツールでは、この6項目を必ずPDFに出力するように作ります。
⚠️ 本記事はPythonでの自動化方法の解説です。登録番号の扱いや税務上の判断は、国税庁のインボイス制度特設サイトや税理士さんに確認してくださいね。
ステップ1:明細と消費税を計算する(端数処理の落とし穴)
最初は計算部分です。ここが重要です👇
- インボイスでは、消費税の端数処理は1枚の請求書につき、税率ごとに1回と決められています
- 明細1行ずつ消費税を計算して切り捨て→合計、というやり方はNG
- お金の計算は
floatではなくDecimalで行い、誤差を防ぐ
下のコードでは、最後にわざと「明細ごとに端数処理するNG例」も計算しています。これ、実行せずに答えられますか? NG例とOK例で、消費税は同じ金額になるでしょうか…?
from dataclasses import dataclass
from decimal import Decimal, ROUND_DOWN
@dataclass
class LineItem:
name: str # 品目
quantity: Decimal # 数量(時間・件数など)
unit_price: int # 単価(税抜・円)
tax_rate: int = 10 # 税率(%)
@property
def amount(self) -> int:
# 税抜金額(1円未満は切り捨て)
return int((self.quantity * self.unit_price).to_integral_value(ROUND_DOWN))
def summarize(items: list[LineItem]) -> dict[int, dict[str, int]]:
"""税率ごとに税抜合計を出し、消費税は「税率ごとに1回だけ」端数処理する"""
result: dict[int, dict[str, int]] = {}
for item in items:
result.setdefault(item.tax_rate, {"subtotal": 0})
result[item.tax_rate]["subtotal"] += item.amount
for rate, row in result.items():
tax = Decimal(row["subtotal"]) * rate / 100
row["tax"] = int(tax.to_integral_value(ROUND_DOWN)) # 切り捨てで統一
return result
if __name__ == "__main__":
items = [
LineItem("Webアプリ改修(時間単価)", Decimal("12.5"), 4550),
LineItem("API仕様書の作成", Decimal("1"), 30000),
LineItem("保守対応(スポット)", Decimal("3"), 3333),
]
for it in items:
print(f"{it.name}: {it.amount:,}円")
summary = summarize(items)
for rate, row in summary.items():
print(f"{rate}%対象 税抜 {row['subtotal']:,}円 / 消費税 {row['tax']:,}円")
# ⚠️ NG例:明細ごとに消費税を切り捨てて足し合わせる
ng_tax = sum(int(Decimal(it.amount) * it.tax_rate / 100) for it in items)
print(f"明細ごとに端数処理した場合の消費税: {ng_tax:,}円")

1円ズレました! 明細ごとに切り捨てると、56,875円の10%(5,687.5円)と9,999円の10%(999.9円)の端数がそれぞれ捨てられてしまいます。合計してから計算すれば、96,874円 × 10% = 9,687.4円 → 9,687円 です。
ポイントをまとめるとこんな感じです。
LineItemは dataclass で1行分の明細を表現。amountプロパティで税抜金額を計算summarize()は税率ごとに税抜合計を出してから、消費税を1回だけ切り捨て- 端数処理は切り捨て・切り上げ・四捨五入のどれを選んでもよいので、今回は切り捨てで統一
「たった1円」ですが、請求書の金額が取引先の計算と合わないと確認の手間が発生してしまいます。自動化するからこそ、計算ルールをコードで固定しておくのが大事なんですよね。
ステップ2:ReportLabで請求書PDFを描く
次は、計算結果をPDFにします。ReportLab には、段落や表を上から順に積んでいける platypus という仕組みがあります。イメージとしては、「部品(Paragraph・Table)をリストに並べて、最後にまとめて流し込む」感じです。
from dataclasses import dataclass, field
from datetime import date
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
from invoice_calc import LineItem, summarize
# 日本語フォントをPDFに埋め込む(Windows 10以降に標準搭載のBIZ UDゴシック)
FONT = "BIZUDGothic"
pdfmetrics.registerFont(TTFont(FONT, r"C:\Windows\Fonts\BIZ-UDGothicR.ttc", subfontIndex=0))
@dataclass
class Invoice:
number: str # 請求書番号
issue_date: date # 発行日
client: str # 請求先(交付を受ける事業者)
period: str # 取引期間
items: list[LineItem] = field(default_factory=list)
ISSUER = {
"name": "山田 太郎(屋号:ヤマダ開発室)",
"reg_no": "T1234567890123", # 登録番号(ダミー)
"bank": "〇〇銀行 △△支店 普通 1234567",
}
def build_pdf(inv: Invoice, path: str) -> None:
h1 = ParagraphStyle("h1", fontName=FONT, fontSize=20, leading=26)
body = ParagraphStyle("body", fontName=FONT, fontSize=10.5, leading=16)
summary = summarize(inv.items)
total = sum(r["subtotal"] + r["tax"] for r in summary.values())
story = [
Paragraph("請求書", h1),
Spacer(1, 4 * mm),
Paragraph(f"{inv.client} 御中", body),
Paragraph(f"請求書番号:{inv.number} 発行日:{inv.issue_date:%Y年%m月%d日}", body),
Paragraph(f"取引期間:{inv.period}", body),
Paragraph(f"発行者:{ISSUER['name']} 登録番号:{ISSUER['reg_no']}", body),
Spacer(1, 4 * mm),
Paragraph(f"ご請求金額(税込):{total:,}円", ParagraphStyle("t", parent=h1, fontSize=16)),
Spacer(1, 4 * mm),
]
# 明細テーブル
rows = [["品目", "数量", "単価", "税率", "金額(税抜)"]]
for it in inv.items:
rows.append([it.name, f"{it.quantity}", f"{it.unit_price:,}", f"{it.tax_rate}%", f"{it.amount:,}"])
for rate, r in summary.items():
rows.append([f"{rate}%対象 小計", "", "", "", f"{r['subtotal']:,}"])
rows.append([f"消費税({rate}%)", "", "", "", f"{r['tax']:,}"])
rows.append(["合計(税込)", "", "", "", f"{total:,}"])
table = Table(rows, colWidths=[75 * mm, 20 * mm, 25 * mm, 18 * mm, 32 * mm])
table.setStyle(TableStyle([
("FONTNAME", (0, 0), (-1, -1), FONT),
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1e3a8a")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("ALIGN", (1, 1), (-1, -1), "RIGHT"),
("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
("BACKGROUND", (0, -1), (-1, -1), colors.HexColor("#fef3c7")),
]))
story += [table, Spacer(1, 6 * mm), Paragraph(f"お振込先:{ISSUER['bank']}", body)]
SimpleDocTemplate(path, pagesize=A4, leftMargin=18 * mm, rightMargin=18 * mm).build(story)
ここが重要です👇
storyリストに Paragraph(文章)や Table(表)を順に追加し、build()で1枚のPDFに組み立てる- 明細の下に「10%対象 小計」「消費税(10%)」を税率ごとに出力(6項目の④⑤)
- 発行者名と登録番号、「〇〇 御中」の交付先名も必ず入れる(①⑥)
TableStyleでヘッダーを紺色、合計行を薄い黄色にして見やすく
登録番号は「T+13桁の数字」の形式です。コード内の T1234567890123 はダミーなので、ご自身の番号に置き換えてください。
⚠️ つまずきポイント:日本語が消えるPDFになった
実は最初、ネットでよく見かける次の書き方で日本語フォントを設定していました。
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
FONT = "HeiseiKakuGo-W5"
pdfmetrics.registerFont(UnicodeCIDFont(FONT))
フォントファイルが不要で手軽なのですが、できたPDFを別の環境で画像化して確認したところ、日本語の部分だけがすべて空白になり、数字とアルファベットしか表示されませんでした😱
原因を調べるために、pypdf でPDF内のフォント情報を確認してみると、こうなっていました。
- CIDフォント版:
HeiseiKakuGo-W5→ 埋め込みなし(ファイルサイズ約3.6KB) - TTF版:
AAAAAA+BIZ-UDGothic-0→ 埋め込みあり(ファイルサイズ約32KB)
つまり、CIDフォントは「フォント名だけ書いておくので、表示する側で日本語フォントを用意してね」という方式です。表示する側に対応フォントがないと、文字が出ません。取引先がどんな環境でPDFを開くかはわからないので、請求書のように人に送るPDFはフォントを埋め込むのが安全です。
そこで、ステップ2のコードでは TTFont で BIZ UDゴシック(Windows 10以降に標準搭載)を埋め込んでいます。.ttc は複数のフォントをまとめたファイルなので、subfontIndex=0 で1つ目を指定しています。使った文字だけが埋め込まれる(サブセット化)ので、ファイルサイズも30KB台に収まりました。Mac や Linux の場合は、Google Fonts などから入手したTTFファイルのパスに書き換えてください。
ステップ3:CSVから取引先ごとに一括作成する
いよいよ自動化の本番です。1か月分の作業記録を、こんなCSV(work_log.csv)で管理している想定です。
client,item,quantity,unit_price
株式会社サンプル商事,Webアプリ改修(時間単価),12.5,4550
株式会社サンプル商事,API仕様書の作成,1,30000
株式会社サンプル商事,保守対応(スポット),3,3333
合同会社テストラボ,データ集計スクリプト開発,1,80000
合同会社テストラボ,運用サポート(時間単価),4,5000
このCSVを読み込んで、取引先ごとに請求書をまとめて作ります。
import csv
from collections import defaultdict
from datetime import date
from decimal import Decimal
from pathlib import Path
from invoice_calc import LineItem
from invoice_pdf import Invoice, build_pdf
OUT_DIR = Path("invoices")
OUT_DIR.mkdir(exist_ok=True)
# ① CSVを取引先ごとにまとめる
by_client: dict[str, list[LineItem]] = defaultdict(list)
with open("work_log.csv", encoding="utf-8", newline="") as f:
for row in csv.DictReader(f):
by_client[row["client"]].append(
LineItem(row["item"], Decimal(row["quantity"]), int(row["unit_price"]))
)
# ② 取引先ごとに請求書番号を振ってPDFを作る
issue = date(2026, 9, 30)
for i, (client, items) in enumerate(by_client.items(), start=1):
inv = Invoice(
number=f"INV-{issue:%Y%m}-{i:03d}",
issue_date=issue,
client=client,
period="2026年9月1日〜2026年9月30日",
items=items,
)
path = OUT_DIR / f"{inv.number}_{client}.pdf"
build_pdf(inv, str(path))
print(f"作成: {path}(明細{len(items)}件)")
ポイントをまとめるとこんな感じです。
defaultdict(list)で、CSVの行を取引先名ごとに自動でグループ化- 数量は
Decimal(row["quantity"])で文字列から直接変換(12.5時間のような小数も誤差なし) - 請求書番号は
INV-年月-連番の形式で自動採番 - ファイル名に番号と取引先名を入れておくと、後から探しやすい
CSVに行を足すだけで、取引先が何社に増えても作業時間はほぼ変わりません🚀
ステップ4:できたPDFを自動チェックする
最後に、できたPDFに6項目が本当に入っているかをpypdfで文字を読み取って確認します。見た目の確認は人の目でも、「項目の入れ忘れ」は機械に見張らせるのが効率的です。ステップ3の一括作成に続けてこのチェックを実行した結果も、下に載せています。
from pathlib import Path
from pypdf import PdfReader
# インボイスに必要な項目が本文に入っているかをキーワードでチェック
REQUIRED = {
"登録番号": "登録番号:T",
"取引年月日(期間)": "取引期間:",
"取引内容": "品目",
"税率ごとの合計と税率": "10%対象 小計",
"税率ごとの消費税額": "消費税(10%)",
"交付先の名称": "御中",
}
for pdf in sorted(Path("invoices").glob("*.pdf")):
text = PdfReader(pdf).pages[0].extract_text()
missing = [k for k, word in REQUIRED.items() if word not in text]
status = "OK" if not missing else f"不足: {', '.join(missing)}"
total_line = next(line for line in text.splitlines() if "ご請求金額" in line)
print(f"{pdf.name}: {status} / {total_line}")

2社分のPDFができて、どちらも必要項目のチェックはOKでした。サンプル商事の請求額 106,561円は、ステップ1で計算した税抜 96,874円+消費税 9,687円 と一致しています。テストラボ側は 税抜100,000円+消費税10,000円=110,000円 です😊
実際にできたPDFを開くと、タイトル・宛名・請求書番号・発行日・取引期間・登録番号、税込の請求金額、明細の表(小計・消費税・合計つき)、振込先が1ページに収まっていました。
もしテンプレートを改造して登録番号の行を消してしまっても、このチェックで「不足: 登録番号」と表示されるので、送る前に気づけます。
さらに便利にするアイデア
- 毎月自動実行:タスクスケジューラやcronで月末に実行すれば、請求書が勝手に揃う
- 作業記録と連携:Googleスプレッドシートの稼働記録をCSVに書き出して、そのまま入力にする
- 軽減税率への対応:
LineItemのtax_rateに 8 を入れれば、summarize()が税率ごとに小計と消費税を分けて出力します(軽減税率対象の品目にはその旨の表示も追加しましょう) - ロゴや印影の追加:ReportLab の
Imageをstoryに追加すれば画像も入れられます
まとめ
今回は、Python と ReportLab で請求書PDFを自動作成するツールを作りました。要点をおさらいしておきます。
- インボイスの6項目(登録番号・取引年月日・取引内容・税率ごとの合計と税率・税率ごとの消費税額・交付先名)を必ず出力する
- 消費税の端数処理は請求書1枚につき税率ごとに1回。明細ごとに切り捨てると1円ズレることがある
- お金の計算は
Decimalで行う - 人に送るPDFは
TTFontで日本語フォントを埋め込む(CIDフォントは埋め込まれない) - pypdf で必要項目を自動チェックすれば、送信前の入れ忘れを防げる
月末の「請求書どうしよう…」が、コマンド1つで終わるようになると本当にラクになります。「むずかしそう」を「できそう」に変えるきっかけとして、ぜひ自分の取引先データで試してみてください!
こちらも読まれています
📚 関連商品・おすすめ書籍
※本記事にはアフィリエイトリンクが含まれます。





