見積書は、画面で見られるだけでは仕事になりません。取引先に送る、印刷する、記録として保管する――どれも「PDFファイル」が必要です。この第5回では、Ruby on Rails 8で作っている見積・請求管理システムに、見積書のPDF出力を入れます。
前回の第4回:金額・消費税・端数処理を正しく計算するでは、税率ごとに1回だけ端数処理をする TaxCalculator を作りました。今回はその計算結果を、そのまま見積書PDFに載せます。
「見積書をExcelで作ってPDFに変換する作業が毎回面倒。システムからボタン1つでPDFを出せないの?」
先に結論です。Railsの見積PDFは、Prawn(プローン)というRubyのライブラリで、約80行のクラス1つにまとめられます。つまずきやすいのは日本語フォントだけです。 フォントを正しく指定し、PDFの「見た目」と「金額の計算」を分けておけば、画面とPDFで金額がずれる心配もありません。
この記事では、PDF出力の方式の選び方、Prawnでの実装、ダウンロードの仕組みと自動テスト、日本語フォントの注意点を、実際に動かしたコードで解説します。
見積書PDFを作る3つの方式と、Prawnを選んだ理由
Railsで帳票PDFを作る方法は、大きく3つあります。どれが正解というわけではなく、帳票の複雑さと運用のしやすさで選びます。
| 方式 | 仕組み | 向いているケース | 注意点 |
|---|---|---|---|
| Prawn(今回採用) | Rubyのコードで線や文字を描く | 見積書・請求書のような、表中心で定型の帳票 | レイアウトをコードで書く。凝ったデザインは手間 |
| HTMLからPDFへ変換 | 画面のHTMLをブラウザエンジンでPDF化 | 画面のデザインをそのままPDFにしたい | サーバーにブラウザ(Chromium等)が必要で、運用が重くなりがち |
| 外部の帳票サービス・SaaS | 外部に任せる | 帳票の種類が非常に多い | 利用料・データの預け先の確認が必要 |
今回Prawnを選んだのは、サーバーに追加のソフトを入れずに済むからです。Prawnは純Rubyのライブラリなので、bundle install だけで動きます。連載の最終回でKamalによる配備を扱うため、「Dockerイメージを軽く、構成をシンプルに」保てる点を優先しました。
Prawnの最新版(2.5)は2024年3月にRubyGemsで公開されています(執筆時点:2026年10月)。
📰 出典:RubyGems.org「prawn」
仕組み:画面・計算・PDFを分けておく
今回の追加は、次の3か所です。
app/pdfs/estimate_pdf.rb(新規):見積書を受け取り、PDFのバイト列を返すapp/controllers/estimates_controller.rb:/estimates/1.pdfのようにURLの末尾が.pdfならPDFを返すapp/views/estimates/show.html.erb:詳細画面に「PDFをダウンロード」リンクを追加
大事なのは、金額の計算をPDFの中に書かないことです。PDFクラスは第4回の @estimate.tax_calculator を呼ぶだけにしています。計算が1か所なので、画面・PDF・のちの請求書で金額が食い違いません。
実装1:Prawnを追加する
Gemfile に2行足します。prawn-table は、表を描くための拡張です。
# Gemfile
# 見積書PDFの生成(純Ruby。ブラウザや外部コマンドが不要)
gem "prawn", "~> 2.5"
gem "prawn-table", "~> 0.2"
bundle install で入ります。
実装2:日本語フォントを指定する(最大のつまずきポイント)
Prawnの標準フォントは英数字用で、日本語を書こうとするとエラーになります。日本語を出すには、日本語を含むフォントファイル(TTF形式)を自分で指定する必要があります。
フォントの置き場所は、次の順番で探すようにしました。環境変数 PDF_FONT_PATH を指定すれば、本番サーバーでもフォントを差し替えられます。
# app/pdfs/estimate_pdf.rb(抜粋)
class EstimatePdf
class FontNotFound < StandardError; end
FONT_CANDIDATES = [
Rails.root.join("config/fonts/ipaexg.ttf").to_s,
"/usr/share/fonts/opentype/ipafont-gothic/ipag.ttf",
"/usr/share/fonts/truetype/fonts-japanese-gothic.ttf"
].freeze
def self.font_path
([ ENV["PDF_FONT_PATH"] ] + FONT_CANDIDATES).compact.find { |path| File.exist?(path) } ||
raise(FontNotFound, "日本語フォント(TTF)が見つかりません。PDF_FONT_PATH を設定してください")
end
フォントが無いときは、原因が分かるメッセージで止めます。「なぜか文字が出ない」を避けるためです。
使うフォントには、IPAexゴシックのように再配布や商用利用が認められたものを選びます。フォントにはライセンスがあり、PDFに埋め込んで配る場合は特に確認が必要です。サンプルにはフォントファイルを含めていません。使うフォントのライセンスは、開発会社と一緒に確認してください。
📰 出典:一般財団法人 文字情報技術促進協議会「IPAフォント」
実装3:見積書のレイアウトを描く
フォントを登録したら、見出し・宛名・明細表・合計表の順に描きます。
# app/pdfs/estimate_pdf.rb(抜粋)
def render
pdf = Prawn::Document.new(page_size: "A4", margin: 40, info: { Title: @estimate.number, Creator: "mitsumori" })
font = EstimatePdf.font_path
pdf.font_families.update("jp" => { normal: font, bold: font })
pdf.font "jp"
header(pdf)
lines_table(pdf)
totals_table(pdf)
note(pdf)
pdf.render
end
pdf.render が、PDFファイルの中身(バイト列)を返します。明細表は、配列の配列を渡すだけで表になります。
# app/pdfs/estimate_pdf.rb(抜粋)
def lines_table(pdf)
rows = [ %w[品名 数量 単位 単価 税率 金額(税抜)] ]
@estimate.lines.each do |line|
rows << [ line.name, line.quantity, line.unit, yen(line.unit_price), "#{line.tax_rate}%", yen(line.amount) ]
end
pdf.table(rows, header: true, width: pdf.bounds.width, cell_style: { size: 9, padding: 5 },
column_widths: { 1 => 40, 2 => 40, 4 => 40 }) do
row(0).background_color = "EEEEEE"
columns(1).align = columns(3).align = columns(5).align = :right
end
end
合計欄は、第4回の計算結果をそのまま並べるだけです。
# app/pdfs/estimate_pdf.rb(抜粋)
def totals_table(pdf)
calc = @estimate.tax_calculator
rows = calc.groups.map { |g| [ "#{g.rate}%対象(税抜 #{yen(g.subtotal)})の消費税", yen(g.tax) ] }
rows << [ "小計(税抜)", yen(calc.subtotal) ]
rows << [ "消費税", yen(calc.tax) ]
rows << [ "合計(税込)", yen(calc.total) ]
pdf.move_down 10
pdf.table(rows, position: :right, cell_style: { size: 9, padding: 5 }, column_widths: [ 220, 100 ]) do
columns(1).align = :right
row(-1).background_color = "EEEEEE"
end
end
税率ごとの消費税が別行で出るので、取引先が電卓で確かめるときも追いやすくなります。
実装4:ダウンロードの仕組み(コントローラ)
ブラウザで開く画面(HTML)と同じURLに、拡張子違いでPDFを返すようにします。Railsの respond_to を使うと、新しい画面を増やさずに済みます。
# app/controllers/estimates_controller.rb
def show
respond_to do |format|
format.html
format.pdf do
pdf = EstimatePdf.new(@estimate)
disposition = params[:disposition] == "inline" ? "inline" : "attachment"
send_data pdf.render, filename: pdf.filename, type: "application/pdf", disposition: disposition
end
end
end
attachment:ブラウザが「ダウンロード」として保存する(標準)inline:ブラウザ内で表示する(?disposition=inlineを付けたとき)- ファイル名は見積番号(例:
Q-00001.pdf)
ここで押さえたいのが権限です。第2回で作った「ログインしていないと画面を見られない」仕組みは、.pdf のURLにもそのまま効きます。画面だけ守って、PDFのURLは直接開ける、という穴を作らないためです。この確認も、あとのテストに入れています。
詳細画面には、リンクを1行足します。
<%# app/views/estimates/show.html.erb %>
<%= link_to "PDFをダウンロード", estimate_path(@estimate, format: :pdf) %>
動作確認:出力されたPDF
サンプルデータ(コーポレートサイト制作の見積)をPDFにした結果です。税率10%と8%の明細が混在していても、税率ごとの消費税が別行で表示されています。


※ 上の画像は開発環境(Linux)でPDFを画像に変換したものです。使うフォントやPDFビューアによって、実際の見た目は少し異なります。
自動テストで固める
PDFの見た目を自動で確かめるのは難しいので、「PDFとして出力できる」「権限が守られている」「ファイル名が正しい」という壊れやすい部分を固定します。
# test/controllers/estimates_controller_test.rb(抜粋)
test "PDFをダウンロードできる" do
sign_in_as users(:viewer)
get estimate_url(estimates(:website), format: :pdf)
assert_response :success
assert_equal "application/pdf", response.media_type
assert_match(/attachment.*Q-00001\.pdf/, response.headers["Content-Disposition"])
assert response.body.start_with?("%PDF-")
end
test "未ログインではPDFも取得できない" do
get estimate_url(estimates(:website), format: :pdf)
assert_redirected_to new_session_url
end
%PDF- はPDFファイルの先頭に必ずある目印です。全体で60件のテストがすべて通り、RuboCop(コードの書き方のチェック)も指摘なしでした。
つまずきやすい点・注意点
- フォント未設定のエラー:開発環境で動いても、本番サーバーにフォントが無いとPDFが作れません。Dockerイメージにフォントを入れるか、
PDF_FONT_PATHで場所を指定します(第9回のデプロイで扱います)。 - 機種依存の文字:丸数字や一部の旧字体は、フォントに含まれず空白になることがあります。取引先名で使う文字を、事前にテスト印刷して確認します。
- 明細が多いとき:Prawnの表は自動で次のページに続きますが、合計欄だけが次ページに飛ぶ場合があります。明細の行数が多い業務では、実データで確認してください。
- 今回は省略したもの:会社のロゴ・角印、ページ番号、PDFの電子署名・タイムスタンプ(電子帳簿保存法への対応)は扱っていません。法令対応は、国税庁の公式情報を確認のうえ、税理士など専門家に相談してください。
発注者向けメモ:見積書PDF機能を依頼するときの確認点
システムに「見積書PDF出力」を頼むとき、見積もりの前に決めておくと、手戻りが減ります。
発注者がやることチェックリスト
- ☐ 今使っている見積書(Excel・紙)の現物を、開発会社に渡す(社名・住所・振込先・角印の位置が分かるもの)
- ☐ 取引先の社名で使う文字(旧字体・機種依存文字)に、特殊なものがないか洗い出す
- ☐ 見積書の保管ルール(紙か電子か、何年保管か)を、経理や税理士に確認する
- ☐ PDFを「メールで送る」のか「印刷して渡す」のか、使い方を決める(次回のメール送信に関わる)
- ☐ 帳票の種類(見積書・請求書・納品書など)が、将来いくつ必要か洗い出す
開発会社への質問例
- 「今の見積書のレイアウトを、どこまで再現できますか。再現が難しい部分はありますか」
- 「PDFに使うフォントは何ですか。ライセンスは、商用利用や配布に問題ありませんか」
- 「明細が多いとき(50行・100行など)の見え方は、実データで確認してもらえますか」
- 「帳票のレイアウトを変えたいとき、費用はどれくらいの目安で、何日くらいかかりますか」
- 「PDFを後から修正・再発行する場合、過去に出した見積書と中身が変わらない仕組みになっていますか」
工数が増えやすいのは、既存のExcelの見た目を細部まで再現したい場合、角印・ロゴの配置、複数の帳票を同じ仕組みで出す場合です。最初は「標準的なレイアウト」で作り、運用しながら調整する進め方が現実的です。なお、今回のサンプルは「発行したあとに見積の中身を変えると、PDFも変わる」作りです。発行済みの書類を固定する仕組みは、請求書の回(第7回)で考えます。
「PDF出力は、ボタン1つの裏で、フォントと権限と金額の計算が絡んでいると分かりました。見積もりを見るときの質問が増えました」
まとめと次回予告
第5回では、Prawnで見積書のPDFを出力できるようにしました。日本語フォントを明示的に指定すること、金額の計算を TaxCalculator に任せてPDFは描くだけにすること、PDFのURLにも権限の確認を効かせることの3点が要です。
次回の第6回は「メール送信を非同期にする」です。作った見積書PDFを、Action MailerとSolid Queueを使って、画面を待たせずにメールで送る仕組みを作ります。
この連載の記事一覧
この記事は連載「Railsで作る見積・請求管理システム」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。
- 【Railsで作る見積・請求管理システム 第0回】Rails 8の雛形を動かして、Excel見積からの脱却を始める
- 【Railsで作る見積・請求管理システム 第1回】取引先と品目のマスタを作る(scaffoldと入力チェック)
- 【Railsで作る見積・請求管理システム 第2回】ログインを付ける(Rails 8標準の認証と権限の分け方)
- 【Railsで作る見積・請求管理システム 第3回】見積書と明細を作る(Hotwireで明細行をその場で追加)
- 【Railsで作る見積・請求管理システム 第4回】金額・消費税・端数処理を正しく計算する(税率ごとに1回だけ丸める)
- 【Railsで作る見積・請求管理システム 第5回】見積書をPDFで出力する(Prawnで日本語フォントを扱う)(この記事)
- 【Railsで作る見積・請求管理システム 第6回】見積書をメールで送る(Solid Queueで送信を非同期にする)
- 【Railsで作る見積・請求管理システム 第7回】受注から請求書へ変換する(状態遷移と請求番号、インボイスの記載事項)
- 【Railsで作る見積・請求管理システム 第8回】入金管理と督促の一覧を作る(一部入金・期限超過の抽出)










コメント
コメント一覧 (2件)
[…] 前回の第5回:見積書をPDFで出力するで作ったPDFを、そのままメールに添付します。 […]
[…] […]