消費税の税込→税抜で1円ずれる理由 — 10%なら11円に1つ届かない

税抜価格から税込価格を出し、別の画面では税込から税抜に戻す。どちらも「税率を掛ける」「1.1で割る」だけに見えるが、消費税額の1円未満をどう丸めるかを両方向でそろえないと、戻した税抜をもう一度税込にしたとき元の額にならない。

この記事では ハシトシステムの消費税計算ツール のコードを読み、税抜→税込と税込→税抜の往復がどれだけ崩れるか、そもそも作れない税込額がどれだけあるかを、1円〜10万円の全数で数える。最後に、インボイス(適格請求書)での端数処理の単位も国税庁の Q&A で確認する。

ツールは2つの向きで丸め方が違う

ツールの計算部分は次の2行である(index.html の calc() から)。

1
2
if($('mode').value==='ex2in'){ ex=amt; tax=Math.floor(ex*r); inc=ex+tax; }
else { inc=amt; ex=Math.round(inc/(1+r)); tax=inc-ex; }
  • 税抜→税込: 消費税額 税抜 × 税率 の1円未満を切り捨てる。
  • 税込→税抜: 税込 ÷ (1 + 税率) を四捨五入して税抜にし、税込との差を消費税額にする。

ページの解説にもこの2つの丸め方がそのまま書かれている。税込は整数なので、税抜を四捨五入することは、消費税額を(ほぼ)四捨五入することと同じになる。つまり片方は切り捨て、もう片方は四捨五入で、向きによって消費税額の丸め方が違う。

たとえば税込60,000円(10%)では、60000 × 10 / 110 = 5454.54… なので、切り捨てなら消費税は5,454円、ツールの税込→税抜では税抜54,545円・消費税5,455円になる。どちらが正しいというものではなく、後で見るとおり端数処理の方法は事業者が選べる。ただ、同じシステムの中で2つの向きを混ぜると、往復で1円ずれる。

切り捨てでは「作れない税込額」がある

消費税を切り捨てる場合、税込は 税抜 + floor(税抜 × 0.1) = floor(税抜 × 1.1) になる。税抜を1円ずつ増やすと、税込はたいてい1円ずつ増えるが、税抜が10円増えるたびに消費税も1円増えるので、そこで税込は2円跳ぶ。

税抜 消費税(切り捨て) 税込
8 0 8
9 0 9
10 1 11
11 1 12

税込10円になる税抜は存在しない。同じ理由で21円、32円、43円……と、10%では11円に1つ、税込に現れない額がある。8%なら税抜12.5円ごとに消費税が1円増えるので、27円に2つ(13円、26円、40円、53円……)が抜ける。

税込→税抜の画面にこうした額が入力されると、どんな税抜を返しても税抜→税込では元に戻らない。ツールの税込→税抜に10円を入れると税抜9円・消費税1円を返すが、税抜9円を税抜→税込にかけると消費税0円・税込9円になる。

切り捨てと対になる逆算は切り上げ

floor(税抜 × 1.1) = 税込 を満たす税抜は、税込 ≤ 税抜 × 1.1 < 税込 + 1 の範囲にある整数である。範囲の幅は 1 ÷ 1.1 で1より小さいので、整数は多くても1つしかなく、あるならそれは ceil(税込 ÷ 1.1) になる。

浮動小数の 1.1 での割り算を避けるなら、整数だけで次のように書ける。

1
2
// 切り捨てで作った税込から税抜に戻す(rate は 10 や 8 の整数)
const in2ex = (inc, rate) => Math.floor((inc * 100 + 100 + rate - 1) / (100 + rate));

戻した税抜をもう一度 税抜 + floor(税抜 × 税率 / 100) にかけ、元の税込に一致しなければ、その税込は切り捨てでは作れない額である。画面で「この税込額になる税抜はありません」と出すか、近い額を案内するかは仕様で決める。

実際に試す

Node.js 24.11.1(Windows 11)で実行した。依存パッケージは要らない。次の内容を tax-roundtrip.mjs として保存する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// tax-roundtrip.mjs : 消費税の税抜→税込と税込→税抜を往復させ、ずれる額と届かない税込額を数える
const N = 100000;

// ツールの税抜→税込: 消費税額の1円未満を切り捨て
const ex2in = (ex, rate) => ex + Math.floor(ex * rate / 100);
// ツールの税込→税抜: 税抜を四捨五入し、差を消費税額にする
const in2exRound = (inc, rate) => Math.round(inc / (1 + rate / 100));
// 切り捨てと対になる逆算: 税抜 = ceil(税込 × 100 / (100 + 税率))(整数だけで計算)
const in2exCeil = (inc, rate) => Math.floor((inc * 100 + 100 + rate - 1) / (100 + rate));

for (const rate of [10, 8]) {
let mismatch = 0, unreachable = 0, ceilOk = 0;
const firstMis = [], firstUnr = [];
for (let inc = 1; inc <= N; inc++) {
const ex = in2exRound(inc, rate);
if (ex2in(ex, rate) !== inc) {
mismatch++;
if (firstMis.length < 3) firstMis.push(`${inc}->${ex}->${ex2in(ex, rate)}`);
}
const exC = in2exCeil(inc, rate);
if (ex2in(exC, rate) === inc) ceilOk++;
else { unreachable++; if (firstUnr.length < 5) firstUnr.push(inc); }
}
console.log(`${rate}% 四捨五入で戻して往復が崩れる: ${mismatch}/${N} 例 ${firstMis.join(' ')}`);
console.log(`${rate}% ceil で戻して往復が成り立つ: ${ceilOk}/${N} 届かない税込額: ${unreachable} 先頭 ${firstUnr.join(',')}`);
}

// 税込60,000円(10%)から消費税を出す: 切り捨て / 四捨五入
const inc = 60000;
console.log('60000円の消費税 切り捨て', Math.floor(inc * 10 / 110), ' 四捨五入(ツール)', inc - in2exRound(inc, 10));

// 1枚の請求書で、明細ごとに切り捨てて足す(認められない)か、税率ごとに1回切り捨てるか
const lines = [98, 98, 98]; // 税抜、10%
const perLine = lines.reduce((s, x) => s + Math.floor(x * 10 / 100), 0);
const perRate = Math.floor(lines.reduce((s, x) => s + x, 0) * 10 / 100);
console.log('税抜98円×3行 明細ごと', perLine, ' 税率ごとに1回', perRate);
1
node tax-roundtrip.mjs

出力:

1
2
3
4
5
6
10%  四捨五入で戻して往復が崩れる: 45455/100000  例 6->5->5 7->6->6 8->7->7
10% ceil で戻して往復が成り立つ: 90909/100000 届かない税込額: 9091 先頭 10,21,32,43,54
8% 四捨五入で戻して往復が崩れる: 48146/100000 例 7->6->6 8->7->7 9->8->8
8% ceil で戻して往復が成り立つ: 92593/100000 届かない税込額: 7407 先頭 13,26,40,53,67
60000円の消費税 切り捨て 5454 四捨五入(ツール) 5455
税抜98円×3行 明細ごと 27 税率ごとに1回 29

税込を四捨五入で税抜に戻すと、10%では1円〜10万円の45,455件(約45%)で、税抜→税込にかけ直したとき元の税込に戻らない。この数には、そもそも作れない税込額も含まれる。切り上げで戻すと、10%では90,909件で往復が成り立ち、成り立たない9,091件はすべて切り捨てでは作れない税込額である。8%でも同じで、92,593件が成り立ち、7,407件が届かない額だった。

6->5->5 は、税込6円を四捨五入で戻すと税抜5円(6 ÷ 1.1 = 5.45…)になり、税抜5円の消費税は切り捨てで0円なので税込5円に戻る、という意味である。切り上げなら税抜6円になり、消費税0円で税込6円に戻る。

インボイスでは「1枚につき税率ごとに1回」

国税庁の「適格請求書等保存方式に関するQ&A」の問57は、適格請求書に書く消費税額等に1円未満の端数が出る場合、一の適格請求書につき税率ごとに1回の端数処理を行う必要がある、と答えている(根拠として消費税法施行令70条の10と消費税法基本通達1-8-15を挙げている)。同じ回答で、次の2点も書かれている。

  • 切上げ、切捨て、四捨五入などの端数処理の方法は任意に選べる。
  • 個々の商品ごとに消費税額等を計算して1円未満を処理し、その合計を消費税額等として記載することは認められない。

問57の記載例では、税込60,000円(10%対象)を 60,000円 × 10/110 ≒ 5,454円、税込40,000円(8%対象)を 40,000円 × 8/108 ≒ 2,962円 としている。これは切り捨てた値と一致する。

上の出力の最後の行は、この違いを小さな例で見たものである。税抜98円の明細が3行ある請求書で、明細ごとに消費税を切り捨てると9円×3 = 27円、税率ごとに合計294円から1回で出すと29円になる。明細ごとに丸めると、行数が増えるほど合計の消費税額が小さく出やすい。

実装するときの注意

  • 丸め方は両方向で対にする。税抜→税込で消費税を切り捨てるなら、税込→税抜は ceil(税込 × 100 / (100 + 税率)) で戻し、もう一度税込にかけて一致を確かめる。四捨五入で戻すと半数近くで1円ずれる。
  • 作れない税込額を想定する。消費税を切り捨てる場合、10%では11円に1つ、8%では27円に2つの税込額は、どの税抜からも出てこない。入力をそのまま受け付けるなら、その旨を示す。
  • 請求書の消費税は税率ごとの合計から1回で出す。明細ごとの消費税を足した額は、インボイスの消費税額等としては認められない(国税庁 Q&A 問57)。明細ごとの税額を画面に出すのは構わないが、合計欄とは別物として扱う。
  • 整数で計算する。inc / 1.1 のような浮動小数の割り算は、今回の範囲(1円〜100万円、10%と8%)では整数版と結果が一致したが、税率を任意にすると保証できない。円は整数、税率は百分率の整数で持ち、割り算は最後に1回だけにする。

まとめ

  • ハシトシステムの消費税計算ツールは、税抜→税込で消費税を切り捨て、税込→税抜で税抜を四捨五入する。向きによって消費税額の丸め方が違うので、往復すると10%では1円〜10万円の約45%で1円ずれる。
  • 消費税を切り捨てる場合、10%では税込10円・21円・32円のように11円に1つ、作れない税込額がある。切り捨てと対になる逆算は ceil(税込 × 100 / (100 + 税率)) で、それ以外の額では往復が成り立つ。
  • インボイスでは、消費税額等の端数処理は1枚の適格請求書につき税率ごとに1回で、方法(切上げ・切捨て・四捨五入)は任意、商品ごとに丸めて合計するのは不可(国税庁 Q&A 問57)。

法人番号を持つ事業者のインボイス登録番号は「T + 法人番号13桁」になる。法人番号の検査用数字は「法人番号の検査用数字をJSで計算する」で扱った。

ツール: 消費税計算(ハシトシステム)。税抜→税込と税込→税抜を切り替えて、10%・8%・任意の税率で税抜・消費税額・税込を出す。計算はブラウザ内で完結する。