この記事は Zenn に投稿した記事 の転載です。内容は同じものです。
本記事は自作の OSS(MIT ライセンス)の紹介を含みます。 制度の内容は国税庁の公表資料に基づいていますが、最新の情報は必ず一次情報でご確認ください。
2026年10月1日、控除率が変わります
免税事業者などからの課税仕入れについて、本来受けられない仕入税額控除を一定割合だけ認める経過措置があります。この割合が、2026年10月1日から 80% → 70% に変わります。
そして、ここからが本題です。
php
// このコードは、2026年10月1日から間違いになります
$deductible = $taxAmount * 0.8;
もっと厄介なのはこちらです。
php
// 改正前の情報で書かれたコード
$rate = $purchaseDate >= '2026-10-01' ? 0.5 : 0.8;
改正前は「令和8年10月から50%」でした。 ところが令和8年度税制改正で段階が細分化され、70% を経由するスケジュールに変わりました。上のコードは、改正情報を追いかけて書き直した結果として間違っている、という一番たちの悪いパターンです。
改正でどう変わったか
課税仕入れを行った日 控除割合
2023-10-01 〜 2026-09-30 80%
2026-10-01 〜 2028-09-30 70%
2028-10-01 〜 2030-09-30 50%
2030-10-01 〜 2031-09-30 30%
2031-10-01 〜 0%(控除不可)
当初は「80% → 50% → 終了」の3段階でしたが、令和8年度税制改正により 80% → 70% → 50% → 30% → 0% の5段階になりました。終了時期も後ろ倒しになっています。
判定は 課税仕入れを行った日 で行います。請求書の発行日でも、支払日でもありません。2026年9月30日までに行った課税仕入れは 80%、10月1日以後は 70% です。期をまたぐ取引は、請求や納品の事実に基づいてどちらの期間の取引かを整理することになります。
:::message alert 控除割合と適用期間は、実装前に必ず国税庁の公表資料で突き合わせてください。二次情報には改正前の記載が残っているものが多く、私自身も一度は改正前の数字で設計しました。 :::
よくある3つの実装ミス
- 割合を定数で埋め込む
php
private const TRANSITIONAL_RATE = 0.8;
制度が変わるたびに、この定数を探して回ることになります。しかも grep で見つかるとは限りません。0.8 という数値リテラルが直接書かれていることのほうが多いからです。
- 判定日を間違える
php
// 悪い例: 請求書の発行日で判定している
$rate = $this->getTransitionalRate($invoice->issuedAt);
月末締め翌月請求の取引で、9月の仕入れに対する請求書を10月に発行すると、本来 80% のところ 70% で計算されます。課税仕入れを行った日で判定してください。
- 期間の終端を開区間で書く
php
if ($date < '2026-10-01') {
return 0.8;
}
return 0.7; // 2028年以降が考慮されていない
段階が2つしかなかった時代の書き方です。段階が増えた瞬間に破綻します。
テーブルとして外に出す
制度は今後も変わります。前提はそこに置きます。
php
*/ private const SCHEDULE = [ ['from' => '2023-10-01', 'to' => '2026-09-30', 'rate' => '0.80'], ['from' => '2026-10-01', 'to' => '2028-09-30', 'rate' => '0.70'], ['from' => '2028-10-01', 'to' => '2030-09-30', 'rate' => '0.50'], ['from' => '2030-10-01', 'to' => '2031-09-30', 'rate' => '0.30'], ['from' => '2031-10-01', 'to' => null, 'rate' => '0.00'], ]; /** * 課税仕入れを行った日に適用される控除割合を返す。 * * @param DateTimeImmutable $purchaseDate 課税仕入れを行った日 * @return numeric-string 控除割合('0.70' 等) */ public static function rateFor(DateTimeImmutable $purchaseDate): string { $date = $purchaseDate->format('Y-m-d'); foreach (self::SCHEDULE as $period) { $afterStart = $date >= $period['from']; $beforeEnd = $period['to'] === null || $date <= $period['to']; if ($afterStart && $beforeEnd) { return $period['rate']; } } // 定義範囲外。黙って 0 を返さない。 throw new UnsupportedDateException( sprintf('経過措置の適用対象外の日付です: %s', $date) ); } } 設計上のポイントは4つです。 期間を閉区間で持つ。 from と to を両方持ち、>= と <= で判定します。「次の段階の開始日の前日まで」という書き方をすると、段階を追加したときに漏れます。 割合を文字列で返す。 浮動小数点を使いません。0.7 は二進数で正確に表現できないので、金額計算に持ち込むと丸めの直前で誤差が出ます。BCMath に渡す前提なら文字列が素直です。 範囲外は例外にする。 制度開始前(2023年9月30日以前)の日付を渡されたとき、0.8 を返してはいけません。呼び出し側のバグを握りつぶすことになります。 SCHEDULE_VERSION を持つ。 これがないと「このコードは改正を反映済みなのか」が分かりません。ライブラリを使う側にとっても、自分の環境がどこまで追随しているかの判断材料になります。 テストは境界日だけ書けばいい このロジックのバグは、ほぼすべて境界日に出ます。 php public function test_境界日の控除割合(): void { $cases = [ '2023-10-01' => '0.80', // 制度開始日 '2026-09-30' => '0.80', // 80% の最終日 '2026-10-01' => '0.70', // 改正日 '2028-09-30' => '0.70', '2028-10-01' => '0.50', '2030-09-30' => '0.50', '2030-10-01' => '0.30', '2031-09-30' => '0.30', '2031-10-01' => '0.00', // 経過措置終了 ]; foreach ($cases as $date => $expected) { $this->assertSame( $expected, TransitionalMeasure::rateFor(new DateTimeImmutable($date)), "境界日 {$date} の判定が誤っています" ); } } public function test_制度開始前は例外(): void { $this->expectException(UnsupportedDateException::class); TransitionalMeasure::rateFor(new DateTimeImmutable('2023-09-30')); } 各段階の最終日と翌日をペアで書くのがコツです。片方だけだと、区間の判定が < と <= のどちらでも通ってしまいます。 割合以外にも要件があります 控除割合だけ直せば済む話ではない点も、あわせて。 帳簿への記載が必要です。 経過措置の適用を受けるには、帳簿に「経過措置の適用を受ける旨」を記載し、請求書等を保存する必要があります。記載のない帳簿では控除が認められません。会計システム側の項目が足りているか確認してください。 適用に限度額が入りました。 令和8年度税制改正で、年間・相手先ごとに税込1億円を超える部分については経過措置の適用外となる、という限度が設けられています。これは請求書1枚だけでは判定できません。 相手先ごとの年間累計を持っている必要があります。ライブラリ単体で完結する話ではないので、システム全体でどこが責任を持つか設計しておく必要があります。 ライブラリでの扱い 上記の実装は、公開している jp-invoice にそのまま入っています。 bash composer require foovar/jp-invoice php use Foovar\JpInvoice\TransitionalMeasure; TransitionalMeasure::rateFor(new DateTimeImmutable('2026-09-30')); // '0.80' TransitionalMeasure::rateFor(new DateTimeImmutable('2026-10-01')); // '0.70' TransitionalMeasure::SCHEDULE_VERSION; // '2026-08-19' MIT ライセンス、依存パッケージなし(ext-bcmath のみ)。消費税額の計算本体(端数処理は税率ごとに1回、というあれです)も同じパッケージに入っています。 GitHub: https://github.com/foovar-toku/jp-invoice 動かして試せるデモ: https://invoice.pij.systems/ 端数処理の話は前回書きました。あわせてどうぞ。 まとめ 2026年10月1日から、経過措置の控除割合が 80% → 70% に変わる 改正前の情報では「50%」。二次情報が錯綜しているので一次情報を確認する 判定は課税仕入れを行った日。請求書の発行日ではない 割合はコードに埋め込まず、期間と割合のテーブルとして外に出す テストは各段階の最終日と翌日をペアで書く 割合以外に、帳簿への記載要件と、年間1億円の限度額がある 残り6週間です。自分のコードに 0.8 が直接書かれていないか、grep してみてください。