フリーランスのための請求書・見積書自動化ガイド — Python + Notionで月末の事務作業をゼロにする
月末になるたびに憂鬱になる請求書作業。案件ごとに金額を確認して、Wordを開いて、PDF変換して……その時間、Pythonで丸ごと自動化できます。
月末に3時間消えていく話
フリーランスを始めて気づくのは、「技術的な仕事より事務作業のほうが精神的にキツい」ということです。請求書を作るだけなのに、なぜか毎月2〜3時間が溶けていく。原因は大体こんな感じです。
- —先月の請求書ファイルを探して、複製して、数字を書き換える
- —案件ごとに単価や作業時間を確認して合計を計算する
- —Word → PDF変換、ファイル名を整えて、メール添付して送る
- —見積書の書式が案件ごとにバラバラになってきた問題
これ、全部Pythonで自動化できます。Notionで案件管理しているなら、ボタン1つで請求書PDFが生成されて指定フォルダに保存される、という状態を作れます。この記事でその仕組みを丸ごと公開します。
Notionで案件管理をしているなら
請求書自動化のベースになるNotionテンプレートを使うと、この記事のコードがそのまま動きます。案件管理・請求書管理・収支ダッシュボードが一体になった構成です。
フリーランスOS — Notion案件管理テンプレート ¥1,980reportlabで請求書PDFを自動生成する
Pythonで請求書PDFを作るなら reportlab が定番です。細かいレイアウト制御ができて、日本語フォントにも対応しています。まずはインストールから。
pip install reportlab
日本語を使うには、日本語フォント(IPAexゴシックなど)が必要です。IPAフォント公式サイトからダウンロードして、スクリプトと同じフォルダに置いてください。
以下が実際に動く請求書生成スクリプトです。コピペして自分の情報に書き換えるだけで使えます。
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")
drawString(x, y, text) のx・yはページ左下が原点(ポイント単位)。mm をインポートすれば 20 * mm のようにミリ単位で指定できて直感的です。
Notion APIと連携して案件データから自動生成する流れ
Notionで案件管理をしているなら、わざわざ請求書のデータを手入力する必要はありません。Notion APIで案件データを取得して、そのまま請求書生成に流し込みます。
まず notion-client をインストール。
pip install notion-client python-dotenv
Notion側の準備は3ステップです。
- Notion Integrationsでインテグレーションを作成し、APIトークンを取得
- 案件管理データベースのページを開き、右上「...」→「コネクト」からインテグレーションを追加
- データベースのURLから
DATABASE_IDをコピー(notion.so/xxx/以降の32文字)
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 ファイルに書いておきます。
NOTION_TOKEN=secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NOTION_DATABASE_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SENDER_NAME=山田 太郎
SENDER_ADDRESS=東京都新宿区△△4-5-6
SENDER_EMAIL=yamada@example.com
見積書テンプレートのカスタマイズ
請求書と見積書は構造がほぼ同じです。タイトルと有効期限の扱いだけ違います。上の generate_invoice() を流用して、引数でドキュメントタイプを切り替える方法が一番シンプルです。
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に実行する例です。
# 毎月末日(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で設定するより確実です。
$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
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)まとめ
月末の請求書作業がなくなると、思ったより精神的な余裕が変わります。「今月はいくら請求したっけ」という不安もなくなるし、見積書を出すスピードが上がると受注率も少し変わったりします。
この記事で作ったもの
- reportlab製の請求書PDF生成スクリプト — コピペで動く完全版
- Notion API連携スクリプト — 「請求待ち」案件を一括PDF化、ステータスも自動更新
- 見積書テンプレートの切り替え方法 — doc_type引数で請求書と見積書を共用
- cron / タスクスケジューラ設定 — 月末自動実行のセットアップ手順
Pythonに慣れていない方は、まず generate_invoice() だけを手動で動かすところから始めてみてください。ダミーデータを入れてPDFが生成されたら、あとは自分のデータに書き換えるだけです。
Notion連携が難しそうなら、フリーランス向けのNotionセットアップガイドから先に読んでおくのがおすすめです。Notionの案件管理DBを整えてからAPIを繋ぐと、格段にスムーズにいきます。