TechPlex Blog 記事一覧
フリーランス

フリーランスのための請求書・見積書自動化ガイド — Python + Notionで月末の事務作業をゼロにする

月末になるたびに憂鬱になる請求書作業。案件ごとに金額を確認して、Wordを開いて、PDF変換して……その時間、Pythonで丸ごと自動化できます。

月末に3時間消えていく話

フリーランスを始めて気づくのは、「技術的な仕事より事務作業のほうが精神的にキツい」ということです。請求書を作るだけなのに、なぜか毎月2〜3時間が溶けていく。原因は大体こんな感じです。

  • 先月の請求書ファイルを探して、複製して、数字を書き換える
  • 案件ごとに単価や作業時間を確認して合計を計算する
  • Word → PDF変換、ファイル名を整えて、メール添付して送る
  • 見積書の書式が案件ごとにバラバラになってきた問題

これ、全部Pythonで自動化できます。Notionで案件管理しているなら、ボタン1つで請求書PDFが生成されて指定フォルダに保存される、という状態を作れます。この記事でその仕組みを丸ごと公開します。

Notionで案件管理をしているなら

請求書自動化のベースになるNotionテンプレートを使うと、この記事のコードがそのまま動きます。案件管理・請求書管理・収支ダッシュボードが一体になった構成です。

フリーランスOS — Notion案件管理テンプレート ¥1,980

reportlabで請求書PDFを自動生成する

Pythonで請求書PDFを作るなら reportlab が定番です。細かいレイアウト制御ができて、日本語フォントにも対応しています。まずはインストールから。

Terminal
pip install reportlab

日本語を使うには、日本語フォント(IPAexゴシックなど)が必要です。IPAフォント公式サイトからダウンロードして、スクリプトと同じフォルダに置いてください。

以下が実際に動く請求書生成スクリプトです。コピペして自分の情報に書き換えるだけで使えます。

invoice_generator.py
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from datetime import datetime, timedelta
import os

# 日本語フォントを登録(IPAexゴシックを使用)
pdfmetrics.registerFont(TTFont('IPAexGothic', './ipaexg.ttf'))

def generate_invoice(invoice_data: dict, output_path: str):
    """
    請求書PDFを生成する

    invoice_data の例:
    {
        "invoice_no": "2026-031",
        "issue_date": "2026-03-31",
        "due_date": "2026-04-30",
        "client_name": "株式会社サンプル",
        "client_address": "東京都渋谷区〇〇1-2-3",
        "sender_name": "山田 太郎",
        "sender_address": "東京都新宿区△△4-5-6",
        "sender_email": "yamada@example.com",
        "items": [
            {"name": "Webサイト制作(デザイン)", "qty": 1, "unit": "式", "unit_price": 150000},
            {"name": "コーディング実装", "qty": 40, "unit": "時間", "unit_price": 5000},
        ],
        "tax_rate": 0.10,
        "note": "お振込みの手数料はご負担いただきますようお願いいたします。"
    }
    """
    c = canvas.Canvas(output_path, pagesize=A4)
    width, height = A4

    def set_font(size, bold=False):
        c.setFont('IPAexGothic', size)

    # タイトル
    set_font(20)
    c.drawCentredString(width / 2, height - 30 * mm, "請 求 書")

    # 請求書番号・発行日
    set_font(9)
    c.drawRightString(width - 20 * mm, height - 42 * mm, f"請求書No.  {invoice_data['invoice_no']}")
    c.drawRightString(width - 20 * mm, height - 48 * mm, f"発行日    {invoice_data['issue_date']}")
    c.drawRightString(width - 20 * mm, height - 54 * mm, f"支払期限  {invoice_data['due_date']}")

    # 宛先
    set_font(11)
    c.drawString(20 * mm, height - 48 * mm, invoice_data['client_name'] + " 御中")
    set_font(9)
    c.drawString(20 * mm, height - 55 * mm, invoice_data.get('client_address', ''))

    # 発行者情報
    set_font(9)
    sender_x = width - 20 * mm
    c.drawRightString(sender_x, height - 65 * mm, invoice_data['sender_name'])
    c.drawRightString(sender_x, height - 71 * mm, invoice_data.get('sender_address', ''))
    c.drawRightString(sender_x, height - 77 * mm, invoice_data.get('sender_email', ''))

    # 合計金額の計算
    subtotal = sum(item['qty'] * item['unit_price'] for item in invoice_data['items'])
    tax = int(subtotal * invoice_data['tax_rate'])
    total = subtotal + tax

    # 合計金額ボックス
    c.setFillColorRGB(0.15, 0.39, 0.92)  # primary blue
    c.rect(20 * mm, height - 100 * mm, width - 40 * mm, 14 * mm, fill=1, stroke=0)
    c.setFillColorRGB(1, 1, 1)
    set_font(11)
    c.drawString(25 * mm, height - 95 * mm, "ご請求金額(税込)")
    set_font(14)
    c.drawRightString(width - 25 * mm, height - 95 * mm, f"¥ {total:,}")
    c.setFillColorRGB(0, 0, 0)

    # 明細テーブル ヘッダー
    table_top = height - 110 * mm
    col_x = [20 * mm, 90 * mm, 120 * mm, 145 * mm, 175 * mm]
    headers = ["項目", "数量", "単位", "単価", "小計"]

    c.setFillColorRGB(0.2, 0.2, 0.2)
    c.rect(20 * mm, table_top - 7 * mm, width - 40 * mm, 7 * mm, fill=1, stroke=0)
    c.setFillColorRGB(1, 1, 1)
    set_font(9)
    for i, (x, h) in enumerate(zip(col_x, headers)):
        c.drawString(x + 1 * mm, table_top - 5 * mm, h)
    c.setFillColorRGB(0, 0, 0)

    # 明細行
    row_h = 8 * mm
    for idx, item in enumerate(invoice_data['items']):
        y = table_top - 7 * mm - (idx + 1) * row_h
        if idx % 2 == 0:
            c.setFillColorRGB(0.97, 0.97, 0.97)
            c.rect(20 * mm, y, width - 40 * mm, row_h, fill=1, stroke=0)
        c.setFillColorRGB(0, 0, 0)
        set_font(9)
        line_total = item['qty'] * item['unit_price']
        c.drawString(col_x[0] + 1 * mm, y + 2 * mm, item['name'])
        c.drawRightString(col_x[1] + 20 * mm, y + 2 * mm, str(item['qty']))
        c.drawString(col_x[2] + 1 * mm, y + 2 * mm, item['unit'])
        c.drawRightString(col_x[3] + 20 * mm, y + 2 * mm, f"¥{item['unit_price']:,}")
        c.drawRightString(width - 20 * mm, y + 2 * mm, f"¥{line_total:,}")

    # 小計・消費税・合計
    summary_y = table_top - 7 * mm - (len(invoice_data['items']) + 1) * row_h
    set_font(9)
    c.drawRightString(width - 20 * mm, summary_y - 5 * mm, f"小計  ¥{subtotal:,}")
    c.drawRightString(width - 20 * mm, summary_y - 12 * mm,
                      f"消費税({int(invoice_data['tax_rate'] * 100)}%)  ¥{tax:,}")

    c.setLineWidth(0.5)
    c.line(width - 70 * mm, summary_y - 15 * mm, width - 20 * mm, summary_y - 15 * mm)
    set_font(10)
    c.drawRightString(width - 20 * mm, summary_y - 21 * mm, f"合計  ¥{total:,}")

    # 備考
    if invoice_data.get('note'):
        set_font(8)
        c.drawString(20 * mm, 25 * mm, "【備考】")
        c.drawString(20 * mm, 20 * mm, invoice_data['note'])

    c.save()
    print(f"請求書を生成しました: {output_path}")
    return output_path


# 使い方
if __name__ == "__main__":
    data = {
        "invoice_no": "2026-031",
        "issue_date": "2026-03-31",
        "due_date": "2026-04-30",
        "client_name": "株式会社サンプル",
        "client_address": "東京都渋谷区〇〇1-2-3",
        "sender_name": "山田 太郎",
        "sender_address": "東京都新宿区△△4-5-6",
        "sender_email": "yamada@example.com",
        "items": [
            {"name": "Webサイト制作(デザイン)", "qty": 1, "unit": "式", "unit_price": 150000},
            {"name": "コーディング実装", "qty": 40, "unit": "時間", "unit_price": 5000},
        ],
        "tax_rate": 0.10,
        "note": "お振込みの手数料はご負担いただきますようお願いいたします。"
    }
    generate_invoice(data, "./invoice_2026-031.pdf")
ポイント:reportlabは座標ベースのレイアウトです。drawString(x, y, text) のx・yはページ左下が原点(ポイント単位)。mm をインポートすれば 20 * mm のようにミリ単位で指定できて直感的です。

Notion APIと連携して案件データから自動生成する流れ

Notionで案件管理をしているなら、わざわざ請求書のデータを手入力する必要はありません。Notion APIで案件データを取得して、そのまま請求書生成に流し込みます。

まず notion-client をインストール。

Terminal
pip install notion-client python-dotenv

Notion側の準備は3ステップです。

  1. Notion Integrationsでインテグレーションを作成し、APIトークンを取得
  2. 案件管理データベースのページを開き、右上「...」→「コネクト」からインテグレーションを追加
  3. データベースのURLから DATABASE_ID をコピー(notion.so/xxx/以降の32文字)
notion_to_invoice.py
from notion_client import Client
from invoice_generator import generate_invoice
from dotenv import load_dotenv
import os
from datetime import datetime, date

load_dotenv()

notion = Client(auth=os.environ["NOTION_TOKEN"])
DATABASE_ID = os.environ["NOTION_DATABASE_ID"]

def fetch_billable_projects():
    """
    Notionの案件DBから「請求待ち」ステータスの案件を取得する
    ※プロパティ名は自分のDBに合わせて変更してください
    """
    response = notion.databases.query(
        database_id=DATABASE_ID,
        filter={
            "property": "ステータス",
            "select": {"equals": "請求待ち"}
        }
    )
    return response["results"]


def notion_page_to_invoice_data(page: dict, invoice_no: str) -> dict:
    """Notionのページデータをinvoiceデータフォーマットに変換する"""
    props = page["properties"]

    def get_text(prop_name):
        prop = props.get(prop_name, {})
        if prop.get("type") == "title":
            return prop["title"][0]["plain_text"] if prop["title"] else ""
        if prop.get("type") == "rich_text":
            return prop["rich_text"][0]["plain_text"] if prop["rich_text"] else ""
        return ""

    def get_number(prop_name):
        return props.get(prop_name, {}).get("number", 0) or 0

    def get_select(prop_name):
        sel = props.get(prop_name, {}).get("select")
        return sel["name"] if sel else ""

    # 作業明細(Notionの「明細」リレーションから取得する場合は別途実装)
    # ここではシンプルに1行明細として組み立てる例
    unit_price = get_number("単価")
    quantity = get_number("作業時間") or 1
    unit = get_select("単位") or "式"

    return {
        "invoice_no": invoice_no,
        "issue_date": date.today().strftime("%Y-%m-%d"),
        "due_date": date.today().replace(day=1).strftime("%Y-%m-") + "30",
        "client_name": get_text("クライアント名"),
        "client_address": get_text("クライアント住所"),
        "sender_name": os.environ.get("SENDER_NAME", ""),
        "sender_address": os.environ.get("SENDER_ADDRESS", ""),
        "sender_email": os.environ.get("SENDER_EMAIL", ""),
        "items": [
            {
                "name": get_text("案件名"),
                "qty": quantity,
                "unit": unit,
                "unit_price": unit_price,
            }
        ],
        "tax_rate": 0.10,
        "note": get_text("備考"),
    }


def run_monthly_invoicing(output_dir="./invoices"):
    """月次請求書の一括生成"""
    os.makedirs(output_dir, exist_ok=True)
    pages = fetch_billable_projects()

    if not pages:
        print("請求待ちの案件はありません。")
        return

    print(f"{len(pages)}件の請求書を生成します...")
    ym = datetime.now().strftime("%Y%m")

    for i, page in enumerate(pages, start=1):
        invoice_no = f"{ym}-{i:03d}"
        invoice_data = notion_page_to_invoice_data(page, invoice_no)
        filename = f"invoice_{invoice_no}_{invoice_data['client_name']}.pdf"
        output_path = os.path.join(output_dir, filename)
        generate_invoice(invoice_data, output_path)

        # 生成後、Notionのステータスを「請求済み」に更新
        notion.pages.update(
            page_id=page["id"],
            properties={
                "ステータス": {"select": {"name": "請求済み"}}
            }
        )

    print(f"\n完了。{output_dir}/ に保存しました。")


if __name__ == "__main__":
    run_monthly_invoicing()

環境変数は .env ファイルに書いておきます。

.env
NOTION_TOKEN=secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NOTION_DATABASE_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SENDER_NAME=山田 太郎
SENDER_ADDRESS=東京都新宿区△△4-5-6
SENDER_EMAIL=yamada@example.com
ポイント:Notionのプロパティ名(「ステータス」「クライアント名」など)は自分のDBの設定に合わせてください。Notion APIの詳細はNotion API自動化ガイドに詳しくまとめています。

見積書テンプレートのカスタマイズ

請求書と見積書は構造がほぼ同じです。タイトルと有効期限の扱いだけ違います。上の generate_invoice() を流用して、引数でドキュメントタイプを切り替える方法が一番シンプルです。

estimate_generator.py(差分のみ)
def generate_document(data: dict, output_path: str, doc_type: str = "invoice"):
    """
    doc_type:
      "invoice"  → 請求書
      "estimate" → 見積書
    """
    c = canvas.Canvas(output_path, pagesize=A4)
    width, height = A4

    # タイトルを切り替え
    title = "請 求 書" if doc_type == "invoice" else "見 積 書"
    c.setFont('IPAexGothic', 20)
    c.drawCentredString(width / 2, height - 30 * mm, title)

    # 見積書固有の項目
    if doc_type == "estimate":
        c.setFont('IPAexGothic', 9)
        c.drawRightString(width - 20 * mm, height - 42 * mm,
                          f"見積書No.  {data.get('estimate_no', '')}")
        c.drawRightString(width - 20 * mm, height - 48 * mm,
                          f"発行日    {data.get('issue_date', '')}")
        c.drawRightString(width - 20 * mm, height - 54 * mm,
                          f"有効期限  {data.get('valid_until', '')}")  # 30日後など

    # 以降は共通ロジック(明細・合計欄)を呼び出す
    # ...(省略)

見積書で変えておくべき点をまとめると:

  • 「支払期限」→「見積有効期限」(発行日から30日が一般的)
  • 「ご請求金額」→「お見積金額」に変更
  • 備考に「本見積は受注確定後、正式な注文書をもって成立いたします」などの文言を入れておく

ひな形を一度作ってしまえば、あとは doc_type="estimate" を渡すだけで見積書が出てきます。フォントや色を変えるだけで見た目も簡単に差別化できます。

cronとタスクスケジューラで完全自動化する

スクリプトが動いたら、あとは月末に自動実行されるよう仕掛けるだけです。

Mac / Linux の場合(cron)

ターミナルで crontab -e を実行して以下を追加します。毎月末日の9:00に実行する例です。

crontab
# 毎月末日(28〜31日)の9:00に実行
# ※月の末日が28日の場合もあるので28日固定で設定するのが安全
0 9 28 * * /usr/bin/python3 /Users/yourname/scripts/notion_to_invoice.py >> /tmp/invoice.log 2>&1

Windows の場合(タスクスケジューラ)

PowerShellでコマンド一発で設定できます。GUIで設定するより確実です。

PowerShell(管理者として実行)
$action = New-ScheduledTaskAction `
    -Execute "python" `
    -Argument "C:\scripts\notion_to_invoice.py"

$trigger = New-ScheduledTaskTrigger `
    -Monthly `
    -DaysOfMonth 28 `
    -At "09:00AM"

Register-ScheduledTask `
    -TaskName "MonthlyInvoiceGenerator" `
    -Action $action `
    -Trigger $trigger `
    -RunLevel Highest
注意:自動実行する場合は、スクリプトが生成したPDFを自分でメールに添付して送る作業だけが残ります。メール送信も自動化したい場合は、Python自動化入門のメール送信セクションのコードと組み合わせてください。

freee・マネーフォワードとの比較 — Python自作の正直なメリット

「それ、freeeでいいじゃん」という声は当然あります。正直に書きます。

項目 freee / MFクラウド Python自作
月額コスト ¥1,980〜¥3,850 ¥0
セットアップ時間 30分〜1時間 半日〜1日
カスタマイズ性 テンプレート範囲内 完全自由
Notion連携 Zapier等が必要 ネイティブ連携
確定申告サポート 充実 別途対応が必要
インボイス対応 自動対応 コードに追加実装

正直な結論:年間売上が300万円を超えてくるなら、freeeやマネーフォワードへの月3,000円は十分元が取れます。確定申告の手間を考えると特に。

一方、駆け出しフリーランスでまだ案件数が少ないうちは、Python自作のほうがコスト面で合理的です。何より、自分の業務フローにぴったりフィットした仕組みを作れるのが一番のメリット。SaaSのUIに業務を合わせる逆転現象が起きません。

Notionで案件管理している人なら、SaaSとPythonの間にある「ちょうどいい落とし所」として、このガイドのアプローチはかなり有効です。

Python自動化レシピをもっと見たい方へ

Qiitaに実践的なPython自動化スニペットをまとめています。請求書以外の業務自動化にも使えるコードを公開中。

Python自動化レシピ集(Qiita)

まとめ

月末の請求書作業がなくなると、思ったより精神的な余裕が変わります。「今月はいくら請求したっけ」という不安もなくなるし、見積書を出すスピードが上がると受注率も少し変わったりします。

この記事で作ったもの

  1. reportlab製の請求書PDF生成スクリプト — コピペで動く完全版
  2. Notion API連携スクリプト — 「請求待ち」案件を一括PDF化、ステータスも自動更新
  3. 見積書テンプレートの切り替え方法 — doc_type引数で請求書と見積書を共用
  4. cron / タスクスケジューラ設定 — 月末自動実行のセットアップ手順

Pythonに慣れていない方は、まず generate_invoice() だけを手動で動かすところから始めてみてください。ダミーデータを入れてPDFが生成されたら、あとは自分のデータに書き換えるだけです。

Notion連携が難しそうなら、フリーランス向けのNotionセットアップガイドから先に読んでおくのがおすすめです。Notionの案件管理DBを整えてからAPIを繋ぐと、格段にスムーズにいきます。

その他の開発ツールを探しているなら

ToolPlexではフリーランス・開発者向けのウェブツールを無料で公開しています。

ToolPlexを見てみる

あわせて読みたい