0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで請求書PDFを自動生成する【HTML+CSSテンプレート方式】

0
Posted at

はじめに

「毎月、取引先ごとに請求書をExcelで作って、PDFに変換して、メールで送る」

件数が増えるほど時間を取られ、しかも金額の入力ミスが許されない作業です。これはPythonで自動化できます。

この記事では、HTMLとCSSでレイアウトを作り、データを差し込んでPDF化する方法を紹介します。

なぜHTML+CSS方式なのか

PythonのPDF生成には主に2つのアプローチがあります。

方式 代表ライブラリ 特徴
座標指定 ReportLab 細かく制御できるが、レイアウト変更が大変
HTML→PDF WeasyPrint CSSで書けて、修正が容易

帳票は「項目の位置を少しずらしたい」「ロゴを変えたい」といった調整が頻繁に発生します。座標をコードで指定する方式だと、そのたびに数値を触ることになります。

HTML+CSS方式なら、レイアウトはテンプレートファイルに分離できるため、デザイン調整とロジックが混ざりません。ブラウザで確認しながら作れるのも利点です。


準備

pip install weasyprint jinja2

WeasyPrintはOSによって追加ライブラリが必要です。

# Ubuntu / Debian
sudo apt install libpango-1.0-0 libpangoft2-1.0-0

# macOS
brew install pango

# Windows は公式ドキュメントの GTK インストール手順を参照

Step 1:テンプレートを作る

templates/invoice.html として保存します。{{ }} の部分に後からデータが入ります。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<style>
  @page {
    size: A4;
    margin: 20mm 18mm;
  }
  body {
    font-family: "Noto Sans JP", sans-serif;
    font-size: 10pt;
    color: #222;
  }
  h1 {
    font-size: 20pt;
    letter-spacing: 0.3em;
    text-align: center;
    margin-bottom: 8mm;
  }
  .meta { text-align: right; margin-bottom: 6mm; }
  .to   { font-size: 12pt; border-bottom: 1px solid #222; padding-bottom: 2mm; width: 60%; }
  .total-box {
    margin: 6mm 0;
    padding: 4mm;
    background: #f2f2f2;
    font-size: 14pt;
    font-weight: bold;
  }
  table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 4mm;
  }
  th {
    background: #333;
    color: #fff;
    padding: 2mm;
    font-size: 9pt;
  }
  td {
    border-bottom: 1px solid #ddd;
    padding: 2.5mm 2mm;
  }
  .num { text-align: right; }
  .summary { width: 50%; margin-left: auto; margin-top: 4mm; }
  .summary td { border: none; padding: 1.5mm 2mm; }
  .summary .grand {
    border-top: 2px solid #333;
    font-weight: bold;
    font-size: 12pt;
  }
  .footer { margin-top: 10mm; font-size: 9pt; color: #555; }
</style>
</head>
<body>

<h1>請 求 書</h1>

<div class="meta">
  請求書番号: {{ invoice_no }}<br>
  発行日: {{ issue_date }}
</div>

<div class="to">{{ client_name }} 御中</div>

<div class="total-box">
  ご請求金額: ¥{{ "{:,}".format(total) }}(税込)
</div>

<p>下記の通りご請求申し上げます。</p>

<table>
  <thead>
    <tr>
      <th>品目</th>
      <th style="width:15%">数量</th>
      <th style="width:20%">単価</th>
      <th style="width:20%">金額</th>
    </tr>
  </thead>
  <tbody>
    {% for item in items %}
    <tr>
      <td>{{ item.name }}</td>
      <td class="num">{{ item.qty }}</td>
      <td class="num">¥{{ "{:,}".format(item.price) }}</td>
      <td class="num">¥{{ "{:,}".format(item.qty * item.price) }}</td>
    </tr>
    {% endfor %}
  </tbody>
</table>

<table class="summary">
  <tr><td>小計</td><td class="num">¥{{ "{:,}".format(subtotal) }}</td></tr>
  <tr><td>消費税(10%)</td><td class="num">¥{{ "{:,}".format(tax) }}</td></tr>
  <tr class="grand"><td>合計</td><td class="num">¥{{ "{:,}".format(total) }}</td></tr>
</table>

<div class="footer">
  <strong>お振込先</strong><br>
  {{ bank_info }}<br><br>
  お支払期限: {{ due_date }}<br><br>
  {{ company_name }}<br>
  {{ company_address }}<br>
  {{ company_email }}
</div>

</body>
</html>

Step 2:データを差し込んでPDF化する

from datetime import date, timedelta
from jinja2 import Environment, FileSystemLoader
from weasyprint import HTML

TAX_RATE = 0.10


def build_invoice(data: dict, output_path: str):
    """請求書データからPDFを生成する"""
    # 金額を計算
    subtotal = sum(i["qty"] * i["price"] for i in data["items"])
    tax = int(subtotal * TAX_RATE)
    total = subtotal + tax

    # テンプレートに差し込む
    env = Environment(loader=FileSystemLoader("templates"))
    template = env.get_template("invoice.html")

    html = template.render(
        **data,
        subtotal=subtotal,
        tax=tax,
        total=total,
    )

    # PDFに変換
    HTML(string=html, base_url=".").write_pdf(output_path)
    print(f"作成しました: {output_path}(合計 ¥{total:,})")


# 実行例
invoice = {
    "invoice_no":      "INV-2026-0042",
    "issue_date":      date.today().strftime("%Y年%m月%d日"),
    "due_date":        (date.today() + timedelta(days=30)).strftime("%Y年%m月%d日"),
    "client_name":     "株式会社サンプル",
    "items": [
        {"name": "Webシステム開発",     "qty": 1, "price": 300000},
        {"name": "保守サポート(月額)", "qty": 3, "price": 20000},
    ],
    "bank_info":       "〇〇銀行 △△支店 普通 1234567 カ)サンプル",
    "company_name":    "株式会社サンプル商事",
    "company_address": "東京都〇〇区〇〇 1-2-3",
    "company_email":   "info@example.com",
}

build_invoice(invoice, "invoice_2026-09.pdf")

Step 3:CSVから一括生成する

実務で本当に効くのはここです。取引先リストから全件を一度に生成します。

import csv
from collections import defaultdict
from datetime import date, timedelta


def bulk_generate(csv_path: str):
    """
    CSV形式:
    取引先,品目,数量,単価
    株式会社A,システム開発,1,300000
    株式会社A,保守サポート,3,20000
    株式会社B,データ移行,1,150000
    """
    # 取引先ごとに明細をまとめる
    grouped = defaultdict(list)

    with open(csv_path, encoding="utf-8-sig") as f:
        for row in csv.DictReader(f):
            grouped[row["取引先"]].append({
                "name":  row["品目"],
                "qty":   int(row["数量"]),
                "price": int(row["単価"]),
            })

    ym = date.today().strftime("%Y%m")

    for idx, (client, items) in enumerate(grouped.items(), start=1):
        data = {
            "invoice_no":      f"INV-{ym}-{idx:03d}",
            "issue_date":      date.today().strftime("%Y年%m月%d日"),
            "due_date":        (date.today() + timedelta(days=30)).strftime("%Y年%m月%d日"),
            "client_name":     client,
            "items":           items,
            "bank_info":       "〇〇銀行 △△支店 普通 1234567 カ)サンプル",
            "company_name":    "株式会社サンプル商事",
            "company_address": "東京都〇〇区〇〇 1-2-3",
            "company_email":   "info@example.com",
        }
        # ファイル名に使えない文字を除去
        safe = "".join(c for c in client if c not in r'\/:*?"<>|')
        build_invoice(data, f"output/請求書_{safe}_{ym}.pdf")


bulk_generate("invoices.csv")

取引先20社なら、実行1回で20ファイルが数秒で揃います。


実務で押さえるポイント

① 日本語フォントを埋め込む

サーバー環境で生成すると文字化けすることがあります。フォントを明示的に指定してください。

@font-face {
  font-family: "NotoJP";
  src: url("fonts/NotoSansJP-Regular.ttf");
}
body { font-family: "NotoJP", sans-serif; }

HTML側でローカルフォントを参照する場合、HTML(string=html, base_url=".") の base_url が相対パスの起点になります。

② ページ番号を入れる

複数ページになる帳票では必須です。

@page {
  @bottom-center {
    content: counter(page) " / " counter(pages);
    font-size: 8pt;
    color: #888;
  }
}

③ 明細が多いとき、表のヘッダーを各ページに出す

thead { display: table-header-group; }
tr    { page-break-inside: avoid; }

これで改ページしても見出しが繰り返され、行の途中で切れることもなくなります。

④ 金額計算は必ずPythonで行う

テンプレート内で計算式を書くと、検算もテストもできなくなります。計算はPython側、テンプレートは表示のみと役割を分けてください。

消費税の端数処理(切り捨て・四捨五入)は取引先との取り決めに従う必要があるため、int() で固定せず設定で切り替えられるようにしておくと安全です。

⑤ 生成後は必ず1件目を目視確認する

自動生成は便利ですが、金額の誤りは信用問題に直結します。 本番運用では、

  • 生成件数と対象件数が一致しているか
  • 合計金額の総和が元データと一致するか

をログに出して検証する仕組みを入れておくと安心です。

print(f"生成: {len(grouped)}件 / 総額: ¥{grand_total:,}")

メール送信まで自動化する

生成したPDFをそのまま送付する場合の例です。

import smtplib
from email.message import EmailMessage
from pathlib import Path


def send_invoice(to_addr: str, client: str, pdf_path: str):
    msg = EmailMessage()
    msg["Subject"] = f"【ご請求書】{date.today().strftime('%Y年%m月')}分"
    msg["From"]    = "billing@example.com"
    msg["To"]      = to_addr
    msg.set_content(f"""{client} 御中

平素より大変お世話になっております。
{date.today().strftime('%Y年%m月')}分の請求書を添付にてお送りいたします。

ご確認のほど、よろしくお願い申し上げます。
""")

    pdf = Path(pdf_path)
    msg.add_attachment(
        pdf.read_bytes(),
        maintype="application",
        subtype="pdf",
        filename=pdf.name,
    )

    with smtplib.SMTP_SSL("smtp.example.com", 465) as smtp:
        smtp.login("billing@example.com", "パスワード")
        smtp.send_message(msg)

送信は誤爆が最も怖い処理です。最初は宛先を自分のアドレスに固定してテストし、内容を確認してから本番の宛先に切り替えてください。


まとめ

  • 帳票PDFは HTML+CSS → WeasyPrint が修正しやすく実務向き
  • レイアウトはテンプレートファイルに分離し、ロジックと混ぜない
  • 計算はPython側、テンプレートは表示のみ
  • CSVから一括生成すれば、月末の作業が数秒で終わる
  • ページ番号・ヘッダー繰り返しは @page と display: table-header-group で対応
  • 金額に関わる処理なので、件数・総額の検証ログは必ず入れる

一度テンプレートを作ってしまえば、翌月からは実行するだけです。請求書に限らず、納品書・見積書・報告書など決まった書式の帳票すべてに応用できます。


帳票・書類作成の自動化のご相談

「毎月の請求書発行を自動化したい」「基幹システムのデータから帳票を出力したい」などのご相談を受け付けています。

🌐 https://datarou.com

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?