MENU

問い合わせ


    【Railsで作る見積・請求管理システム 第8回】入金管理と督促の一覧を作る(一部入金・期限超過の抽出)

    請求書を発行したあとの仕事は「ちゃんと入金されたか」の確認です。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回です。連載のほかの回は次のとおりです(連載の一覧ページ)。

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


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

      この記事を書いた人

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

      コメント

      目次