見積書をメールで届けたら、次は「受注が決まったので請求書を出す」場面です。Excelで見積書をコピーして請求書に書き換える運用は、金額の転記ミスや二重発行の原因になりがちです。この第7回では、Ruby on Rails 8で作っている見積・請求管理システムに、受注済みの見積書から請求書を自動で作る機能を入れます。
前回の第6回:見積書をメールで送るでは、メール送信を裏側に回しました。今回は見積書の「状態」を管理し、請求書をデータとして持つところまでを作ります。
「見積書を請求書にするとき、Excelで書き写していて金額が合わなくなったり、同じ請求書を2回出してしまったりして困る」
先に結論です。見積書に「見積中→受注→請求書発行済み」という状態を持たせ、順番どおりにしか進めない作りにします。 請求書は見積書の明細を「写して」別のデータとして保存します。こうすると、二重発行を防げて、発行後に見積書やマスタを直しても過去の請求書の金額が変わりません。
この記事では、状態遷移(=状態が決まった順番でだけ変わる仕組み)の実装、請求番号の付け方、インボイス制度(適格請求書)の記載事項への対応、自動テストまでを、実際に動かしたコードで解説します。
請求書を「見積書の書き換え」で済ませてはいけない理由
見積書の画面に「請求書として表示」ボタンを付けるだけなら簡単です。しかし、見積書そのものを請求書として使い回すと、次の問題が起きます。
| 方式 | 動き | 起きやすい問題 |
|---|---|---|
| 見積書を使い回す | 同じデータを見せ方だけ変える | 受注後に見積書を直すと、発行済みの請求書の金額も変わってしまう。請求日・支払期日・請求番号を持てない |
| 請求書を別データとして作る(※今回採用) | 受注時点の明細を写して、請求書を新しく保存する | 「写す」処理と「二重に作らない」仕組みが要る |
請求書は取引先に渡し、会計や税務の記録にも使う書類です。発行した内容があとから変わらないことが大切なので、別データにして固定します。
仕組み:見積書の状態は一方通行で進める
見積書に状態を持たせ、次の順にだけ進められるようにします。
- 見積中(quoted):作成直後。取引先に見積書を送っている段階
- 受注(ordered):取引先から発注の返事をもらった状態
- 請求書発行済み(invoiced):請求書を作った状態。ここで終わり
「見積中のものだけ受注にできる」「受注済みのものだけ請求書にできる」と決めておくと、請求書の二重発行や、見積中の見積書から請求書ができてしまう事故を、画面の操作ミスがあっても防げます。
Railsでは enum(=決まった選択肢のどれか1つを持つ項目を作る機能)で状態を表せます。
📰 出典:Rails API「ActiveRecord::Enum」
公式ドキュメントでは、enumは選択肢に対応する値を持つ項目を宣言し、ordered? のような確認用のメソッドや invoiced! のような更新用のメソッドを自動で用意する仕組みとされています。
実装1:データベースに状態と請求書の置き場所を作る
まず見積書に状態と受注日時の列を足します(db/migrate/20261005100000_add_status_to_estimates.rb)。
class AddStatusToEstimates < ActiveRecord::Migration[8.1]
def change
# quoted(見積中)→ ordered(受注)→ invoiced(請求書発行済み)
add_column :estimates, :status, :string, null: false, default: "quoted"
add_column :estimates, :ordered_at, :datetime
end
end
次に請求書と請求明細のテーブルです(db/migrate/20261005100100_create_invoices.rb、要点のみ)。
create_table :invoices do |t|
t.references :estimate, null: false, foreign_key: true, index: { unique: true } # 1つの見積書から請求書は1通だけ
t.references :customer, null: false, foreign_key: true
t.string :number
t.string :title, null: false
t.date :issued_on, null: false
t.date :due_on, null: false
# 発行時点の発行事業者名と登録番号を請求書に写す
t.string :issuer_name, null: false
t.string :registration_number, null: false
t.text :note
t.timestamps
end
add_index :invoices, :number, unique: true
ポイントは2つあります。
estimate_idに一意(ユニーク)制約を付けています。アプリ側の確認をすり抜けても、データベースが「同じ見積書から2通目の請求書」を拒否します- 発行事業者名と登録番号は、請求書の側に写して保存します。会社名や登録番号を後で変更しても、過去の請求書は発行時のままになります
請求明細(invoice_lines)は、見積明細と同じ項目(品名・数量・単価・税率)を持つ別テーブルです。
実装2:状態を進めるメソッドを、モデルに書く
状態を進める処理は、画面(コントローラー)ではなくモデルに書きます。ルールをモデルに集めておくと、画面が増えても、テストでも同じルールが守られるからです(app/models/estimate.rb)。
has_one :invoice, dependent: :restrict_with_error
enum :status, { quoted: "quoted", ordered: "ordered", invoiced: "invoiced" }, default: :quoted
class InvalidTransition < StandardError; end
# 受注にする。見積中のものだけ。同時に2回押されても二重に進めないよう、行をロックして状態を読み直す
def place_order!(at: Time.current)
with_lock do
raise InvalidTransition, "見積中の見積書だけを受注にできます(現在: #{status_label})" unless quoted?
update!(status: :ordered, ordered_at: at)
end
end
with_lock は、その見積書の行をデータベース上でロック(=処理が終わるまで他の人が同時に触れないようにする)して、最新の状態を読み直してから中の処理を実行します。
📰 出典:Rails API「ActiveRecord::Locking::Pessimistic」
公式ドキュメントでは、with_lock はトランザクションを開始し、レコードをロックして読み込み直したうえでブロックを実行するものと説明されています。ボタンの二度押しや、2人が同時に操作したときに、二重に請求書ができる事故を防ぐ用途に向いています。
請求書を作るメソッドは次のとおりです。
def issue_invoice!(issued_on: Date.current, due_on: nil, registration_number: Invoice.registration_number_from_env)
with_lock do
raise InvalidTransition, "受注済みの見積書だけ請求書にできます(現在: #{status_label})" unless ordered?
create_invoice!(
customer: customer, title: title, note: note,
issued_on: issued_on, due_on: due_on || issued_on.next_month.end_of_month, # 既定は翌月末払い
issuer_name: Invoice.issuer_name_from_env, registration_number: registration_number,
lines: lines.map { |l| InvoiceLine.new(l.slice(:position, :name, :unit, :quantity, :unit_price, :tax_rate)) }
)
invoiced!
invoice
end
end
- 見積書の明細を
InvoiceLineにコピーして請求書に持たせています - 請求書の作成と、見積書の状態を「請求書発行済み」にする更新は、
with_lockのトランザクションの中で行います。途中で失敗したら、どちらも元に戻ります - 支払期日の既定は「発行日の翌月末」としています。実際の締め日・支払条件は取引先ごとに違うため、サンプルでは引数で変えられるようにしてあります(画面からの指定は未実装)
- 発行事業者名と登録番号は環境変数(
ISSUER_NAME、INVOICE_REGISTRATION_NUMBER)から読み、コードに実値を書きません
実装3:請求番号は保存後のIDから作る
請求番号は、見積番号(Q-00012)と同じ考え方で、保存後のIDから作ります(app/models/invoice.rb)。
after_create :assign_number
private
# 請求番号は保存後のIDから作る(例: INV-00012)。重複しない
def assign_number
update_column(:number, format("INV-%05d", id))
end
number には一意制約を付けてあるので、重複はありません。ただし、IDは作成に失敗した分も進むことがあり、番号が飛ぶ(欠番が出る)ことがあります。インボイス制度では、請求書番号は「連番であること」が条件ではなく、後から探しやすい一意の番号であればよいとされています。厳密に連番にしたい場合は、年ごとの採番テーブルを作るなどの工夫が必要で、本連載では扱いません。
実装4:インボイス(適格請求書)の記載事項を満たす
インボイス制度の請求書に必要な記載事項は、国税庁の資料で次のように整理されています。
📰 出典:国税庁「インボイス制度の概要」
国税庁の説明では、インボイスには、交付を受ける相手方の氏名又は名称、売手の氏名又は名称及び登録番号、取引年月日、取引内容(軽減税率の対象品目である旨)、10%・8%それぞれの対価の額及び適用税率、10%・8%それぞれの消費税額等を記載するとされています。登録番号は「T」から始まる13桁の数字です。
これをサンプルの請求書にどう対応させたかを表にします。
| 記載事項 | サンプルでの対応 |
|---|---|
| 相手方の名称 | 請求先として取引先名を表示(customer.name) |
| 売手の名称と登録番号 | issuer_name と registration_number を請求書に保存して表示 |
| 取引年月日 | 発行日(issued_on)で代用(※下記の注意を参照) |
| 取引内容(軽減税率の対象である旨) | 税率8%の品目に「※」を付け、「※は軽減税率の対象品目です」と注記 |
| 税率ごとの税抜金額・適用税率 | 第4回で作った TaxCalculator の税率ごとの集計をそのまま表示 |
| 税率ごとの消費税額 | 同上。税率ごとに1回だけ端数処理した結果 |
登録番号の形式は、モデルで検証します(app/models/invoice.rb)。
# インボイス制度の登録番号は「T」+13桁の数字
REGISTRATION_NUMBER_FORMAT = /\AT\d{13}\z/
validates :registration_number, format: { with: REGISTRATION_NUMBER_FORMAT }
形式が正しくなければ請求書は保存されず、見積書の状態も「受注」のまま残ります(自動テストで確認しています)。なお、これは形式の確認だけで、番号が本当に登録されているかの確認(国税庁の公表サイトとの照合)は行いません。
請求書の画面側では、軽減税率の品目に「※」を付けます(app/views/invoices/show.html.erb)。
<td><%= line.name %><%= "※" if line.tax_rate == 8 %></td>
...
<% if @invoice.lines.any? { |l| l.tax_rate == 8 } %><p class="muted">※ は軽減税率(8%)の対象品目です。</p><% end %>
注意: 取引年月日が発行日と異なる(納品日と請求日が違う、月末にまとめて請求するなど)場合は、取引年月日の列を別に持つ必要があります。サンプルでは簡略化のため発行日で代用しています。税務上の要件の最終的な判断は、顧問税理士や国税庁の最新の案内で確認してください。
実装5:画面とルーティング
画面の操作は2つのボタンだけです。見積中なら「受注にする」、受注済みなら「請求書を発行する」を出します(app/views/estimates/show.html.erb)。
<% if Current.user.can_edit? %>
<% if @estimate.quoted? %>
<%= button_to "受注にする", order_estimate_path(@estimate), form: { data: { turbo_confirm: "この見積書を受注にしますか?" } } %>
<% elsif @estimate.ordered? %>
<%= button_to "請求書を発行する", estimate_invoice_path(@estimate), form: { data: { turbo_confirm: "請求書を発行しますか?(発行後は取り消せません)" } } %>
<% end %>
<% end %>
ログイン済みでも、閲覧のみの権限の人にはボタン自体を出さず、サーバー側(コントローラー)でも権限を確認します(第2回の仕組み)。画面で隠すだけでは、URLを直接叩かれたときに防げないためです。
ルーティング(config/routes.rb)は次の追加だけです。
resources :estimates do
post :deliver, on: :member
post :order, on: :member
resource :invoice, only: :create # POST /estimates/:estimate_id/invoice
end
resources :invoices, only: %i[ index show ]
請求書は「見積書から作る」ものなので、作成は見積書の下のURLにし、請求書の一覧・詳細だけを独立させています。コントローラー(app/controllers/invoices_controller.rb)は、モデルのメソッドを呼んで、ルール違反の例外を画面のメッセージに変えるだけです。
def create
invoice = @estimate.issue_invoice!
redirect_to invoice, notice: "請求書 #{invoice.number} を発行しました。", status: :see_other
rescue Estimate::InvalidTransition => e
redirect_to @estimate, alert: e.message, status: :see_other
end
動作確認:自動テストで「やってはいけないこと」を固定する
状態遷移は、正しい操作が動くことより「やってはいけない操作が止まること」を確かめるのが大切です。test/models/estimate_status_test.rb で次を確認しています。
test "見積中のままでは請求書にできない" do
assert_no_difference "Invoice.count" do
assert_raises(Estimate::InvalidTransition) { estimates(:website).issue_invoice! }
end
end
test "請求書は二重に発行されない" do
estimate = estimates(:ordered)
estimate.issue_invoice!(registration_number: "T1234567890123")
assert_no_difference "Invoice.count" do
assert_raises(Estimate::InvalidTransition) { estimate.issue_invoice!(registration_number: "T1234567890123") }
end
end
test "発行後に見積書の明細を変えても、請求書は変わらない" do
estimate = estimates(:ordered)
invoice = estimate.issue_invoice!(registration_number: "T1234567890123")
estimate.lines.first.update!(unit_price: 1)
assert_equal 165_000, invoice.reload.total
end
コントローラーのテスト(test/controllers/invoices_controller_test.rb)では、担当者が「受注にする→請求書を発行する」まで進められること、閲覧のみの人は受注にも発行にもできないことを確認しています。
実行すると、既存のテストを含めて次の結果になりました(Ruby 3.3・Rails 8・PostgreSQL 16の開発環境)。
cd blogs/it_hacchu/series/rails-mitsumori/code
bin/rails test
# 78 runs, 231 assertions, 0 failures, 0 errors, 0 skips
コード整形のチェック(RuboCop)も指摘なしです。なお、この環境では請求書のPDF出力は作っていません。ブラウザで実際に画面を操作する確認も行っていないため、画面は自動テスト(HTMLの出力内容)で確認した範囲です。
つまずきやすい点・注意
- 発行済みの請求書を直したくなったとき:このサンプルは「発行後は変更しない」前提で、修正や取消の機能はありません。実務では、間違えた請求書を取り消して新しく出し直す(訂正の請求書を出す)運用が必要です。訂正の扱いは税務の要件が絡むため、顧問税理士に確認してください
- 同じ見積書を、部分的に請求したい場合:分割請求(着手金・中間金・残金など)には、この「1見積書につき1請求書」の作りでは足りません。請求書を複数持てるようデータの作りを変える必要があります
- 請求番号の連番:前述のとおり、欠番が出ます。会計ソフトの連携や社内ルールで連番が必須なら、最初に決めておきましょう
- 電子帳簿保存法:電子で作った請求書の保存には、別途要件があります。本連載では扱いません
発注者向けメモ:請求書機能を開発会社に頼むときの確認点
「請求書は『見積書の見た目違い』ではなく、税務の記録になる書類です。最初に決めておくことが意外と多いんです。」
発注者がやることチェックリスト
- 請求書の記載項目(登録番号、軽減税率の表記など)を、顧問税理士に確認してもらってから依頼する
- 請求のパターンを洗い出す(1回請求のみか、分割請求・月額請求・締め日ごとの請求があるか)
- 発行済みの請求書を間違えたときの手順(取消・訂正)を、業務として決めておく
- 請求番号のルール(連番が必要か、年度で区切るか)を決める
- 誰が受注を確定でき、誰が請求書を発行できるか(権限)を決める
- 会計ソフトに取り込みたい場合は、その形式を依頼前に伝える
開発会社への質問例
- 「請求書を発行した後に、見積書やマスタを直しても、発行済みの請求書の金額が変わらない作りになっていますか?」
- 「同じ請求書が二重に発行されないよう、データベース側でも防ぐ仕組みはありますか?」
- 「分割請求や月額請求に対応する場合、今の作りから追加でどのくらいの作業が増えますか?」
- 「請求書の取消・訂正は、どのような操作と記録の残し方になりますか?」
- 「インボイスの記載事項について、どの項目を、どの画面・帳票で満たしているか一覧で教えてもらえますか?」
請求書の機能は、画面の数が少なく見えても、パターン(分割・月額・訂正)が増えるほど作業が大きく変わる部分です。最初の相談で「請求のパターン」を伝えておくと、見積もりのズレを減らせます。
まとめと次回予告
この第7回では、見積書に状態(見積中→受注→請求書発行済み)を持たせ、受注済みの見積書から請求書を別データとして作る機能を入れました。ポイントは次の4つです。
- 状態は一方通行で進め、ルールは画面ではなくモデルに書く
with_lockと一意制約で、二重発行を2段構えで防ぐ- 請求書は見積書の明細を写して固定し、発行後に変わらないようにする
- インボイスの記載事項を請求書の項目に対応づけ、登録番号の形式を検証する
次の第8回では、入金管理と督促の一覧を作ります。請求書ごとの入金状況を記録し、支払期日を過ぎても入金されていない請求書を一覧で抜き出します。
この連載の記事一覧
この記事は連載「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回】入金管理と督促の一覧を作る(一部入金・期限超過の抽出)










コメント
コメント一覧 (1件)
[…] 第8回では、前回の記事で作った請求書に入金を記録し、期限を過ぎても入金がない請求書を一覧で抜き出す仕組みを、Ruby on Rails 8で作ります。前回は受注から請求書へ変換するところまで進めました。 […]