MENU

問い合わせ


    【Railsで作る見積・請求管理システム 第4回】金額・消費税・端数処理を正しく計算する(税率ごとに1回だけ丸める)

    見積書や請求書で一番「間違えてはいけない」のは、じつは明細の入力ではなく合計の計算です。1円ずれただけで、取引先から「電卓で計算したら合わない」と連絡が来ることもあります。この第4回では、Ruby on Rails 8で作っている見積・請求管理システムに、消費税の計算と端数処理を入れます。

    前回の第3回:見積書と明細を作るでは、見積書(親)と明細行(子)を作り、画面で明細を追加・削除できるようにしました。ただし金額は「税抜の小計」だけで、消費税はまだ計算していませんでした。

    「見積書の消費税が、Excelで作ったものとシステムで1円違う。どっちが正しいのか分からず困っています」

    先に結論です。消費税は「明細1行ごと」ではなく「税率ごとの合計」に対して、1回だけ端数処理をします。 計算は1か所(専用のクラス)に集め、金額はすべて整数の「円」で扱い、結果を自動テストで固定しておく。この3つを守れば、1円のずれはほぼ防げます。

    この記事では、税率ごとの集計と端数処理の考え方、Railsでの実装(約30行)、Minitestでの固め方を、実際に動かしたコードで解説します。

    目次

    消費税の1円ずれが起きる3つの理由

    まず、なぜ1円ずれるのかを整理します。原因が分かると、何を決めておけばよいかも見えてきます。

    理由1:明細ごとに丸めるか、合計で丸めるかが違う

    たとえば、105円の品物を10%の税率で3行入力したとします。1行あたりの消費税は10.5円です。

    計算方法計算消費税の合計
    明細1行ごとに切り捨て10円 × 3行30円
    税率ごとの合計に1回だけ切り捨て315円 × 10% = 31.5円 → 切り捨て31円

    同じ明細なのに1円違います。Excelの表を明細ごとに計算してきた会社のシステムと、合計で計算するシステムでは、ここでずれが生まれます。

    理由2:端数処理の方式(切り捨て・四捨五入・切り上げ)が違う

    端数の扱いには、切り捨て・四捨五入・切り上げがあります。どれが「正しい」というより、会社や取引先ごとに決めて、全書類で統一するものです。ここが決まっていないまま作り始めると、あとから「うちは四捨五入です」と言われて作り直しになります。

    理由3:小数・浮動小数点で計算している

    プログラムで金額を小数(たとえば 0.1 を足し合わせるような計算)で扱うと、コンピュータ内部の表現の都合で、ごくわずかな誤差が出ることがあります。金額は小数を使わず、円を整数で持って整数のまま計算するのが基本です。

    インボイス制度の「税率ごとに1回」ルール

    2023年10月から始まったインボイス制度(適格請求書等保存方式)では、請求書に税率ごとに区分した消費税額を書きます。このとき、端数処理は請求書1枚につき、税率ごとに1回とされています。明細1行ずつ丸めてから足し合わせる方法は認められていません。

    📰 出典:国税庁「インボイス制度の理解のために」(令和8年4月)

    見積書そのものは、インボイス制度の記載義務の対象となる書類ではありません。ただ、見積書の金額がそのまま請求書の金額になることが多いので、見積書の段階から請求書と同じ計算方法にそろえておくと、あとで「見積と請求で金額が違う」というトラブルを避けられます。この連載でも、第7回で請求書を作る時にそのまま同じ計算を使います。

    なお、端数を切り捨てにするか四捨五入にするかは、事業者が選べます(回数が「税率ごとに1回」であることが要件です)。税の扱いは個別の事情で変わるため、実際の運用は税理士や税務署などの専門家に確認してください。この記事は一般的な考え方の紹介です。

    実装:計算を1か所に集める

    実装の方針は、次の3つです。

    • 消費税の計算は TaxCalculator(税計算専用のクラス)にだけ書く。画面やモデルに計算式を散らさない
    • 明細を税率ごとにグループ分けし、グループの税抜合計に対して端数処理を1回だけ行う
    • 金額は整数だけで計算し、Float(小数)や BigDecimal は使わない

    1. TaxCalculator を作る

    app/models/tax_calculator.rb の全体です。

    class TaxCalculator
      # 端数処理の方式。引数は「税抜合計 × 税率(%)」で、100で割る時に端数が出る
      ROUNDINGS = {
        floor: ->(numerator) { numerator / 100 },          # 切り捨て
        round: ->(numerator) { (numerator + 50) / 100 },   # 四捨五入
        ceil:  ->(numerator) { (numerator + 99) / 100 }    # 切り上げ
      }.freeze
    
      # 税率ごとの集計1行分
      Group = Data.define(:rate, :subtotal, :tax) do
        def total = subtotal + tax
      end
    
      attr_reader :groups
    
      def initialize(lines, rounding: :floor)
        @rounding = ROUNDINGS.fetch(rounding) { raise ArgumentError, "unknown rounding: #{rounding}" }
        @groups = build_groups(lines)
      end
    
      def subtotal = groups.sum(&:subtotal)
      def tax = groups.sum(&:tax)
      def total = subtotal + tax
    
      private
        def build_groups(lines)
          lines.group_by(&:tax_rate).sort.reverse.map do |rate, rate_lines|
            subtotal = rate_lines.sum(&:amount)
            Group.new(rate: rate, subtotal: subtotal, tax: @rounding.call(subtotal * rate))
          end
        end
    end

    ポイントを3つ説明します。

    ① 整数の割り算で端数処理を表現する

    Rubyでは整数どうしの割り算は、小数点以下が切り捨てられます。そのため、税額は 税抜合計 × 税率(%) ÷ 100 を整数の割り算で求めれば、そのまま「切り捨て」になります。四捨五入は50を足してから割る、切り上げは99を足してから割る、という昔からの整数計算の技です。小数がまったく出てこないので、誤差も出ません。

    ② 税率ごとのグループ分けは group_by

    lines.group_by(&:tax_rate) で、明細を税率(8%・10%)ごとに分けます。sort.reverse は、画面で「10%対象」→「8%対象」の順に並べるためです。

    ③ Data.define で集計1行分を不変の値にする

    Group は Ruby 3.2 から標準で使える Data.define(=中身を後から書き換えられない小さな値のクラス)で作っています。集計結果が途中で書き換わらないので、計算結果の取り扱いが安全になります。

    📰 出典:Ruby 3.3 リファレンス「Data」

    2. 見積書モデルから呼び出す

    app/models/estimate.rb では、計算を TaxCalculator に任せるだけにします。

      # 税率ごとの集計・消費税・税込合計(計算の本体は TaxCalculator)
      def tax_calculator
        TaxCalculator.new(active_lines)
      end
    
      delegate :subtotal, :tax, :total, to: :tax_calculator

    delegate は「この名前で呼ばれたら、あちらのオブジェクトに任せる」というRailsの書き方です。estimate.total と書くだけで税込合計が取れます。active_lines は、画面上で「削除」した明細(保存前に削除マークが付いた行)を除いた明細のことです。

    端数処理の方式は、今回は標準で「切り捨て」にしています。取引先ごとに変えたい場合は、TaxCalculator.new(lines, rounding: :round) のように渡せる作りにしてあります。取引先マスタに設定項目を足す部分は、必要になった段階で追加します。

    3. 画面に税率ごとの内訳を出す

    app/views/estimates/show.html.erb の表の下部(tfoot)に、税率ごとの内訳・小計・消費税・税込合計を出します。

    <tfoot>
      <% calc = @estimate.tax_calculator %>
      <% calc.groups.each do |group| %>
        <tr class="tax-group">
          <th colspan="5" class="num"><%= group.rate %>%対象(税抜 <%= number_with_delimiter(group.subtotal) %>円)の消費税</th>
          <td class="num"><%= number_with_delimiter(group.tax) %>円</td>
        </tr>
      <% end %>
      <tr><th colspan="5" class="num">小計(税抜)</th><td class="num"><%= number_with_delimiter(calc.subtotal) %>円</td></tr>
      <tr><th colspan="5" class="num">消費税</th><td class="num"><%= number_with_delimiter(calc.tax) %>円</td></tr>
      <tr class="grand-total"><th colspan="5" class="num">合計(税込)</th><td class="num"><%= number_with_delimiter(calc.total) %>円</td></tr>
    </tfoot>

    一覧画面(index.html.erb)の金額欄も「小計(税抜)」から「合計(税込)」に変えました。サンプルデータ(画面デザイン費 300,000円・10%と、打合せ用弁当 5,000円・8%)では、10%対象の税が30,000円、8%対象の税が400円で、合計は335,400円になります。

    Minitestで計算結果を固める

    税の計算は、あとから誰かが「ちょっと直した」ときに壊れやすい部分です。そこで、期待する数字をテストとして残します。test/models/tax_calculator_test.rb から抜粋します。

    test "税率ごとに税抜金額を集計して税を計算する" do
      calc = TaxCalculator.new(lines([ 300_000, 10 ], [ 5_000, 8 ]))
      assert_equal [ 10, 8 ], calc.groups.map(&:rate)
      assert_equal [ 30_000, 400 ], calc.groups.map(&:tax)
      assert_equal 335_400, calc.total
    end
    
    test "端数処理は税率ごとの合計に対して1回だけ行う(明細ごとに丸めない)" do
      # 105円×10%=10.5円の明細が3行。行ごとに切り捨てると30円、合計に1回だけなら31円
      calc = TaxCalculator.new(lines([ 105, 10 ], [ 105, 10 ], [ 105, 10 ]))
      assert_equal 31, calc.tax
    end
    
    test "端数処理は切り捨て・四捨五入・切り上げを選べる" do
      data = lines([ 105, 10 ])  # 税額は10.5円
      assert_equal 10, TaxCalculator.new(data, rounding: :floor).tax
      assert_equal 11, TaxCalculator.new(data, rounding: :round).tax
      assert_equal 11, TaxCalculator.new(data, rounding: :ceil).tax
    end

    このほか、明細がない場合はすべて0になること、知らない端数処理の名前を渡したらエラーにすること、見積書モデル経由でも同じ結果になること、画面に内訳が出ることをテストしています。

    動作確認

    サンプルプロジェクトで次のコマンドを実行し、すべて通ることを確認しました。

    cd blogs/it_hacchu/series/rails-mitsumori/code
    bin/rails test      # 54 runs, 153 assertions, 0 failures
    bin/rubocop         # no offenses detected

    確認環境は Ruby 3.3系・Rails 8系・PostgreSQL 16です(執筆時点:2026年10月)。ブラウザでの画面表示そのものは、今回は自動テスト(HTMLの内容の検証)での確認にとどまります。

    つまずきやすい点・注意

    • 値引き・端数調整の行:「出精値引き」のようなマイナスの行は、今回のサンプルでは入力できません(単価は0以上)。値引きを入れる場合は、税率ごとの合計がどう変わるかを先に決める必要があります
    • 数量が小数の明細:「1.5時間」のような小数の数量は今回の対象外です。入れる場合は、金額(数量×単価)の端数をどこで処理するかも決めます
    • 税込で単価を決める商品:税込価格から逆算する場合は、計算の順序が変わります。サンプルは「税抜単価」前提です
    • 消費税率の変更:税率はいまのところ8%と10%だけを許可しています。将来の改正では、税率の追加や、期間ごとの税率の切り替えが必要になります

    発注者向けメモ:金額計算を頼む時の確認点

    見積・請求システムでは、画面の見た目よりも「計算ルールを最初に決めること」が費用と品質を左右します。

    発注者がやること チェックリスト

    • ☐ 端数処理の方式(切り捨て・四捨五入・切り上げ)を、社内の経理・取引先との慣行を確認して決めたか
    • ☐ 現在Excelで使っている見積書・請求書の計算結果と、新しいシステムの結果を何件か突き合わせる計画があるか
    • ☐ 税率が8%と10%の混在、値引き行、小数の数量など、実際の業務に出てくる特殊なパターンを開発会社に伝えたか
    • ☐ 見積書と請求書で、同じ計算ルールを使うことを確認したか
    • ☐ 税の扱いで迷うところを、税理士など専門家に確認する窓口があるか

    開発会社への質問例

    • 「消費税の端数処理は、明細ごとですか、税率ごとの合計ですか。請求書で税率ごとに1回という考え方に合わせられますか」
    • 「端数処理の方式(切り捨て・四捨五入など)は、取引先ごとに変えられますか。変えた場合の追加工数はどのくらいですか」
    • 「計算結果が正しいことを、どんなテストで確認していますか。そのテストの内容を見せてもらえますか」
    • 「金額は小数ではなく整数で計算していますか。1円のずれが出たときは、どこを調べればよいですか」
    • 「消費税率が将来変わったとき、過去に作った見積書・請求書の金額は変わらないようにできますか」

    工数が増えやすいのは、取引先ごとの端数処理の切り替え、値引き行や小数の数量への対応、税込価格からの逆算、過去の書類の税率を保ったままの改定対応です。初回は「税抜単価・税率ごとに1回の端数処理」という標準的なルールで作り、実際の業務で必要になった特殊なパターンを少しずつ足していく進め方がおすすめです。

    「1円のずれの正体が、丸める回数と方式を決めていなかったことだと分かりました。まずは社内の運用ルールを確認します」

    まとめと次回予告

    第4回では、消費税の計算を TaxCalculator に集め、税率ごとの合計に対して端数処理を1回だけ行う仕組みをRails 8で作りました。金額は整数だけで計算し、切り捨て・四捨五入・切り上げを選べる作りにして、Minitestで計算結果を固めています。

    次回の第5回は「見積書をPDFで出力する」です。日本語フォントを扱うPDFの作り方と、見積書のひな形、ダウンロードの仕組みを取り上げます。

    この連載の記事一覧

    この記事は連載「Railsで作る見積・請求管理システム」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。

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


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

      この記事を書いた人

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

      コメント

      コメント一覧 (3件)

      目次