この記事は Zenn に投稿した記事 の転載です。内容は同じものです。
本記事は自作の OSS(MIT ライセンス)の紹介を含みます。
3行の請求書で1円ズレる
税抜105円の商品を3行、税率10%、切捨て。消費税はいくらでしょうか。
$tax = 0;
foreach ($lines as $line) {
$tax += floor($line->amount * 0.1); // 105 × 10% = 10.5 → 10
}
// $tax = 30
これは制度違反です。 正しくは 31 円になります。
税率ごとに合計してから1回だけ丸める
315 × 10% = 31.5 → 31
国税庁「インボイス制度に関するQ&A」問57 の(注)にこう書かれています。
一の適格請求書に記載されている個々の商品ごとに消費税額等を計算し、1円未満の端数処理を行い、
その合計額を消費税額等として記載することは認められません。
根拠は消費税法施行令第70条の10と、消費税法基本通達1-8-15です。
何がまずいのか
1円の差が出るだけなら丸め誤差の話ですが、これは買手の仕入税額控除の額に影響します。
自社の帳簿では合っているのに取引先と1円合わない、という形で表面化して、原因究明に半日溶けます。
さらに厄介なのは、このバグはテストで気付きにくいことです。
単価が 100 円や 1,000 円のテストデータだと端数が出ないので、ずっと緑のまま本番に出ます。
実装するときの3つのルール
1. 丸めの単位は「税率グループ」
10%対象と8%対象をそれぞれ合計し、それぞれ1回ずつ丸めます。
税率をまたいで合計してから丸めるのも誤りです。
2. 丸め方は選べるが、継続適用する
切上げ・切捨て・四捨五入のどれを採るかは事業者が決められます(問57)。
ただし請求書ごとに変えてよいという意味ではないので、
事業者の設定として持ち、請求書単位では上書きできない構造にしておくのが安全です。
3. float を使わない
0.1 は二進数で正確に表現できません。丸めの直前で 31.499999... になり、
四捨五入の結果が変わることがあります。PHP なら BCMath、金額は文字列と整数で扱います。
// 悪い例
$tax = round($base * 0.1);
// BCMath で
$tax = bcmul((string) $base, '0.10', 10); // 丸めは自前で(bcmath に丸め関数は無い)
ライブラリを書きました
同じ実装を何度も書くのが嫌になったので、計算部分だけを切り出して公開しました。
composer require foovar/jp-invoice
use Foovar\JpInvoice\{Issuer, Invoice, LineItem};
use Foovar\JpInvoice\Enum\{TaxRate, RoundingMode};
$issuer = new Issuer('自社', 'T1234567890123', RoundingMode::FLOOR);
$invoice = new Invoice($issuer);
$invoice->addLine(new LineItem('商品A', '1', '105', TaxRate::STANDARD_10));
$invoice->addLine(new LineItem('商品B', '1', '105', TaxRate::STANDARD_10));
$invoice->addLine(new LineItem('商品C', '1', '105', TaxRate::STANDARD_10));
$invoice->calculate()->totalTaxAmount; // 31
- MIT ライセンス、依存パッケージなし(
ext-bcmathのみ) - PHP 8.2 / 8.3 / 8.4 で CI green、PHPStan level max
- float 不使用。CI で
src/への混入を機械的に検査 - 税抜入力・税込入力の両方、適格簡易請求書、適格返還請求書、登録番号の検証、経過措置の控除割合
国税庁Q&A の計算例をそのままテストにしています。 問57 の記載例(税込10万円のうち10%対象6万円 →
5,454円、8%対象4万円 → 2,962円)や、法人番号公表サイトのチェックディジット検算例などです。
- GitHub: https://github.com/foovar-toku/jp-invoice
- 動かして試せるデモ: https://invoice.pij.systems/
ついでに踏んだ罠をいくつか
個人事業者の登録番号はチェックディジットを満たさない
登録番号は T + 13桁で、法人の場合は法人番号と一致します。法人番号には
チェックディジットがあるので検証したくなりますが、個人事業者の登録番号は法人番号ではないため、
この規則に従いません。形式検証(エラー)とチェックディジット検証(参考情報)は分ける必要があります。
返還インボイスの「1万円未満」は税率ごとに判定しない
振込手数料相当額の値引きなどで返還インボイスを出すとき、税込1万円未満なら交付義務が免除されます。
この判定は「請求や債権の単位ごと」であり、適用税率ごとではありません(問28(注))。
8%対象6,000円 + 10%対象5,000円 の返品は、税率ごとに見ると両方1万円未満ですが、
合計11,000円なので交付義務があります。
経過措置の割合は令和8年度改正で段階が変わった
免税事業者等からの課税仕入れの控除割合は、改正前は「令和8年10月から50%」でしたが、
改正後は 70% → 50% → 30% → 0% と段階を踏みます。二次情報には改正前の記載が
残っているものが多いので、一次情報で確認してください。
あわせて「年間・相手先ごとに税込1億円を超える部分は適用外」という限度額も入りました。
まとめ
- 端数処理は一の請求書につき税率ごとに1回
- 丸め方は選べるが継続適用。事業者単位で固定する
- float を使わない
- 制度の細部は二次情報が錯綜しているので、国税庁の公表資料に当たる
同じところで困っている人の役に立てば幸いです。