見積書や請求書で一番「間違えてはいけない」のは、じつは明細の入力ではなく合計の計算です。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(=中身を後から書き換えられない小さな値のクラス)で作っています。集計結果が途中で書き換わらないので、計算結果の取り扱いが安全になります。
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回です。連載のほかの回は次のとおりです(連載の一覧ページ)。
- 【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件)
[…] 前回の第4回:金額・消費税・端数処理を正しく計算するでは、税率ごとに1回だけ端数処理をする TaxCalculator を作りました。今回はその計算結果を、そのまま見積書PDFに載せます。 […]
[…] 【Railsで作る見積・請求管理システム 第4回】金額・消費税・端数処理を正… […]
[…] 【Railsで作る見積・請求管理システム 第4回】金額・消費税・端数処理を正… […]