請求書を発行したあとの仕事は「ちゃんと入金されたか」の確認です。Excelで請求書を管理していると、入金の確認が担当者の記憶や通帳のチェック頼みになり、督促の漏れにつながりがちです。
第8回では、前回の記事で作った請求書に入金を記録し、期限を過ぎても入金がない請求書を一覧で抜き出す仕組みを、Ruby on Rails 8で作ります。前回は受注から請求書へ変換するところまで進めました。
「請求書は出したけれど、入金されたかどうかの確認が追いつかない。督促を忘れて回収が遅れるのが怖い」
先に結論です。「入金済み/未入金」を保存せず、入金の合計から毎回計算する設計にすると、一部入金や入金の取り消しでもズレない入金管理が、少ないコードで作れます。期限超過も同じ考え方で、「支払期日を過ぎていて、未入金額が残っている」請求書として抜き出します。
この記事の内容は、次の4つです。
- 入金(Payment)の持たせ方と、一部入金の扱い
- 入金状態を「保存せず計算する」理由
- 期限超過の判定(期日当日の扱い)と督促一覧
- 権限による操作の制限と、テストでの固定
今回作るもの:入金の記録と督促の一覧
完成イメージは次のとおりです。
| 画面 | できること |
|---|---|
| 請求書の詳細 | 入金の登録(分割入金も可)、入金履歴の表示、未入金額・期限超過日数の表示 |
| 請求書の一覧 | 請求額・未入金額・入金状況(未入金/一部入金/入金済み)の表示 |
| 期限超過の一覧 | 支払期日を過ぎて未入金額が残る請求書だけを、期日の古い順に表示 |
権限は第2回のルールを引き継ぎ、閲覧のみ=見るだけ、担当者=入金を登録できる、管理者=入金を取り消せる、とします。
入金テーブルを作る:1つの請求書に複数回の入金
入金は、請求書とは別のテーブルにします。1つの請求書に対して「半分だけ先に入金された」「手数料が引かれて数回に分かれた」といったことがあるため、1対多の関係にします。
# db/migrate/20261006030000_create_payments.rb
class CreatePayments < ActiveRecord::Migration[8.1]
def change
create_table :payments do |t|
t.references :invoice, null: false, foreign_key: true
t.date :paid_on, null: false
t.integer :amount, null: false
t.string :note
t.timestamps
end
add_index :payments, :paid_on
add_check_constraint :payments, "amount > 0", name: "payments_amount_positive"
end
end
ポイントは最後の add_check_constraint です。Railsの入力チェック(バリデーション)に加えて、データベース側にも「入金額は0より大きい」という制約を入れています。画面以外(スクリプトや手作業のSQL)からデータが入る場合にも、不正な値を防げるためです。出典は、Railsガイドのマイグレーションの説明です。
📰 出典:Rails ガイド「Active Record マイグレーション」
入金状態は「保存せず計算する」
次に、「入金済みか」をどう判断するかです。請求書テーブルに status(入金済み/未入金)の列を持たせる方法が思い浮かびますが、今回は列を作りません。
理由は、入金の記録と状態の二重管理になるからです。入金を取り消したのに状態が「入金済み」のまま残る、といったズレは、状態を保存している限り起こりえます。入金の合計から毎回求めれば、ズレようがありません。
# app/models/invoice.rb(抜粋)
has_many :payments, -> { order(:paid_on, :id) }, dependent: :destroy, inverse_of: :invoice
# 入金済み額と未入金額。入金状態は保存せず、入金の合計から毎回求める(二重管理によるズレを防ぐ)
# build 中(未保存)の入金は数えない
def paid_amount = payments.select(&:persisted?).sum(&:amount)
def balance = total - paid_amount
# unpaid: 未入金 / partial: 一部入金 / paid: 入金済み
def payment_status
if balance <= 0 then :paid
elsif paid_amount.positive? then :partial
else :unpaid
end
end
total は、第4回で作った税込の合計(TaxCalculator)です。請求額の計算は1か所に集まっているので、入金管理側は「請求額から入金の合計を引く」だけで済みます。
なお select(&:persisted?) は、保存前の入金を数えないためのものです。最初の実装ではこれを入れておらず、「未入金額ちょうどの入金」を登録しようとすると、画面から作りかけの入金まで合計に含まれて、登録が拒否されるという不具合がテストで見つかりました。小さな業務ロジックほど、境界の値(ちょうど全額)をテストに入れる価値があります。
入金額のチェック:請求額を超える入金は登録させない
入金のモデルは次のとおりです。
# app/models/payment.rb
class Payment < ApplicationRecord
belongs_to :invoice
validates :paid_on, presence: true
validates :amount, presence: true, numericality: { only_integer: true, greater_than: 0 }
validate :must_not_exceed_balance, on: :create
private
# 請求額を超える入金は登録させない(過入金は返金・次回相殺など、運用で別に扱う)
def must_not_exceed_balance
return if invoice.nil? || amount.blank?
errors.add(:amount, "が未入金額(#{invoice.balance}円)を超えています") if amount > invoice.balance
end
end
請求額を超えて入金された場合(過入金)は、返金するか次回の請求と相殺するかなど、会社ごとの運用になります。連載のサンプルでは、まず登録できないようにして、画面で気づけるようにしています。
期限超過の判定:期日当日はまだ期限内
期限超過は「支払期日を過ぎていて、入金が済んでいない」ことです。ここで決めておきたいのが期日当日の扱いです。このサンプルでは、期日当日はまだ期限内、翌日から超過としました。
# app/models/invoice.rb(抜粋)
# 支払期日を過ぎても入金が済んでいない(期日当日はまだ期限内)
def overdue?(today = Date.current) = !paid? && due_on < today
def overdue_days(today = Date.current) = overdue?(today) ? (today - due_on).to_i : 0
# 期限超過の請求書(まず期日で絞ってから、入金状態は Ruby 側で判定する)
def self.overdue(today = Date.current)
includes(:lines, :payments, :customer).where(due_on: ...today).order(:due_on, :id).select { |i| i.overdue?(today) }
end
today を引数にしているのは、テストで「今日の日付」を差し替えやすくするためです。日付に依存する処理は、日付を外から渡せる形にしておくと、期日当日・翌日・1か月後といった境界をテストで確かめられます。
入金状態は保存していないため、SQLだけでは「入金済みか」を判定できません。そこで、期日で絞り込んだ(件数の少ない)請求書だけを読み込み、Ruby側で判定しています。件数が数万件を超える規模では、入金済み額を請求書の列に持つ、集計のビューを作るなどの見直しが必要です。連載のサンプルは、小さな会社の規模を想定しています。
請求書の一覧と督促リスト
一覧画面は、同じ画面で絞り込みを切り替えます。
# app/controllers/invoices_controller.rb(抜粋)
def index
# ?filter=overdue で、期限を過ぎても入金がない請求書(督促の対象)だけを、期日の古い順に出す
@overdue_only = params[:filter] == "overdue"
@invoices = if @overdue_only
Invoice.overdue
else
Invoice.includes(:customer, :lines, :payments).order(issued_on: :desc, id: :desc)
end
end
ビュー(app/views/invoices/index.html.erb)では、期限超過の行に overdue クラスを付けて背景を変え、「◯日超過」を表示しています。
<tr class="<%= "overdue" if invoice.overdue? %>">
...
<td class="num"><%= number_with_delimiter(invoice.balance) %>円</td>
<td>
<%= invoice.payment_status_label %>
<% if invoice.overdue? %><strong class="overdue-badge">(<%= invoice.overdue_days %>日超過)</strong><% end %>
</td>
</tr>
督促の文面の作成や自動送信まではこの連載では扱いません。まずは「誰に・いくら・何日超過か」が一目で分かる一覧があれば、担当者は確認と連絡に集中できます。
入金の登録と取り消し:権限で操作を分ける
入金の登録は担当者以上、取り消しは管理者だけにしました。取り消しは、帳簿のつじつまが変わる操作だからです。権限の仕組みは第2回の require_permission をそのまま使います。
# app/controllers/payments_controller.rb(抜粋)
class PaymentsController < ApplicationController
require_permission :can_edit?, only: :create
require_permission :can_destroy?, only: :destroy
before_action :set_invoice
def create
payment = @invoice.payments.build(params.expect(payment: %i[ paid_on amount note ]))
if payment.save
redirect_to @invoice, notice: "#{payment.amount}円の入金を登録しました。", status: :see_other
else
redirect_to @invoice, alert: payment.errors.full_messages.to_sentence, status: :see_other
end
end
...
end
ルーティングは、請求書の下に入金を置いています(config/routes.rb)。
resources :invoices, only: %i[ index show ] do
resources :payments, only: %i[ create destroy ]
end
params.expect は、Rails 8で使える、必要なパラメータだけを取り出す書き方です。想定外の項目が混ざっても受け取らない(マスアサインメントの防止)ため、入金のように金額を扱う画面で特に役立ちます。
テストで境界の値を固定する
入金管理は、「ちょうど全額」「期日の翌日」といった境界で間違えやすい機能です。第4回と同じように、Minitestで境界を固定しました。
# test/models/invoice_payment_test.rb(抜粋)
test "分割で入金すると一部入金になり、全額で入金済みになる" do
@invoice.payments.create!(paid_on: Date.new(2026, 11, 10), amount: 50_000)
assert_equal :partial, @invoice.reload.payment_status
assert_equal @invoice.total - 50_000, @invoice.balance
@invoice.payments.create!(paid_on: Date.new(2026, 11, 20), amount: @invoice.balance)
assert_equal :paid, @invoice.reload.payment_status
assert_equal 0, @invoice.balance
end
test "支払期日の当日は期限内、翌日から期限超過になる" do
due = @invoice.due_on
assert_not @invoice.overdue?(due)
assert @invoice.overdue?(due + 1)
assert_equal 3, @invoice.overdue_days(due + 3)
end
画面側は、権限と督促一覧を test/controllers/payments_controller_test.rb で確認しています。今回追加した確認は、モデル・コントローラーあわせて13件です。
動作確認の方法
実際に動かして確認する手順です(PostgreSQLが起動している前提)。
cd blogs/it_hacchu/series/rails-mitsumori/code
bundle install
bin/rails db:create db:migrate db:seed
bin/rails test
bin/rubocop
今回の開発環境では、bin/rails test(全91件)と bin/rubocop が通ることを確認しました。ブラウザでの画面確認は、今回の実行環境では行っていません。画面の見た目はご自身の環境で、請求書を発行して入金を登録し、「期限超過のみ」の一覧を開いて確かめてください。
つまずきやすい点
- 入金済みの列を保存したくなる:一覧が速くなるように見えますが、取り消しや修正でズレる原因になります。まずは計算で求め、遅くなってから対策します。
- 期日当日の扱いが決まっていない:「期日を過ぎたら」が当日を含むのか、会社内で決めておかないと、督促のタイミングがバラつきます。
- 振込手数料の扱い:取引先が手数料を差し引いて振り込むことがあります。差額を「値引き」として扱うか、「手数料」の入金として扱うかは、経理の運用と合わせる必要があります。
- 取り消しの履歴が残らない:サンプルは入金を削除するだけです。実務では、誰がいつ取り消したかの監査ログが必要になることがあります(連載では省略)。
発注者向けメモ:入金管理を開発会社に頼むときの確認点
入金管理は、画面の数は少なくても、会社ごとの運用ルールで作業量が変わる機能です。開発会社に相談する前に、次の点を社内で決めておくと、見積もりのブレが小さくなります。
- ☐ 入金は手入力か、銀行の入出金明細(CSV等)から取り込みたいか
- ☐ 分割入金・過入金・振込手数料の差し引きを、どう扱うか
- ☐ 期限超過の起点は「期日の翌日」か「期日当日」か
- ☐ 督促は、一覧を見て人が連絡する運用か、メールなどで自動送信したいか
- ☐ 入金の取り消し・修正を、誰ができるようにするか。履歴を残すか
- ☐ 会計ソフトに入金データを連携したいか
開発会社には、次のような質問をすると、対応範囲が見えやすくなります。
- 「入金の状態は、どのタイミングで・どの情報から決まる設計ですか」
- 「過入金や分割入金のとき、どう動きますか。取り消したときに履歴は残りますか」
- 「銀行の明細データを取り込む場合、照合(どの請求書への入金か)はどう行いますか」
なお、督促の方法や遅延損害金など、法律・税務が関わる点は、一般論を調べたうえで、弁護士や税理士などの専門家にご確認ください。この記事は個別の判断を示すものではありません。
まとめと次回予告
第8回では、請求書への入金を記録し、期限超過の請求書を一覧で抜き出す仕組みを作りました。要点は次の3つです。
- 入金は別テーブルに持たせ、分割入金に対応する
- 入金状態は保存せず、入金の合計から毎回計算する
- 期限超過は「期日を過ぎていて、未入金額が残っている」で判定し、期日当日の扱いは先に決めておく
次回は最終回、第9回「Kamalでサーバーに配備する」(予定)です。ここまで作ったシステムを、Dockerイメージにしてサーバーへ配備する手順と、SSL・秘密情報・運用の注意点を扱います。
この連載の記事一覧
この記事は連載「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回】入金管理と督促の一覧を作る(一部入金・期限超過の抽出)(この記事)










コメント
コメント一覧 (3件)
[…] 【Railsで作る見積・請求管理システム 第8回】入金管理と督促の一覧を作る… […]
[…] 【Railsで作る見積・請求管理システム 第8回】入金管理と督促の一覧を作る… […]
[…] 【Railsで作る見積・請求管理システム 第8回】入金管理と督促の一覧を作る… […]