MENU

問い合わせ


    【Railsで作る見積・請求管理システム 第5回】見積書をPDFで出力する(Prawnで日本語フォントを扱う)

    見積書は、画面で見られるだけでは仕事になりません。取引先に送る、印刷する、記録として保管する――どれも「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%の明細が混在していても、税率ごとの消費税が別行で表示されています。

    見積書PDFの出力例(開発環境での確認画面)

    ※ 上の画像は開発環境(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回です。連載のほかの回は次のとおりです(連載の一覧ページ)。

    システム制作・運用・保守のお問い合わせはこちら


      よかったらシェアしてね!
      • URLをコピーしました!
      • URLをコピーしました!

      この記事を書いた人

      株式会社THIRD HERO代表取締役 朝野貴朗
      Webシステム開発を中心に、toC向けサービスサイトの運営、ツール開発などを行ってまいりました。

      コメント

      目次