e-Tax の xtx に自作の別表を差し込む|配当の明細書 2 枚を Python で生成した設計判断

申告ソフトが出力しない別表を自作して e-Tax の xtx に差し込むとき、設計の要は 3 つだった。丸めを 1 か所に集めて一次資料で確定する、入力を契約で縛る、差し込むときに他の帳票のバイト列を変えない。筆者はこの方針で生成した XML 片を xtx に差し込み、手修正なしで提出して申告を完了した。

筆者の法人は、受け取った配当を益金不算入のルールに従って法人税の計算に反映している。申告ソフト(全力法人税)は、筆者が使った版(2026 年 7 月時点)では、別表六(一)「所得税額の控除に関する明細書」と別表八(一)「受取配当等の益金不算入に関する明細書」を出力しない。haito-forge は、配当受取履歴の CSV からこの 2 枚を xtx(e-Tax に提出する申告データ。中身は XML)に差し込む XML 片として生成し、申告ソフトへ手入力する転記値のサマリも出す自社専用の補助ツールである。Python 3.12 以上・pydantic・lxml・typer 製で、リポジトリは非公開。

本稿は実装の記録で、税務の助言ではない。申告の判断は税理士等に確かめてほしい(免責事項・出典と検証)。様式・e-Tax の仕様書・国税庁の「法人税のあらましと申告の手引」は、いずれも 2026 年 7 月時点で参照したものである。

1. 補助ツールとして切り出す

作ったのは 2 枚の XML 片と転記値サマリだけである。サマリは別表六(一)の控除所得税額計(別表四の加算と別表一の控除税額へ)と別表八(一)の益金不算入額(別表四の減算へ)を出し、会計上の受取配当金との差額を対象外の行で説明する。別表四と別表一は申告ソフトが作るので、値を渡すだけにした。

捨てたのは、申告ソフトが出力する帳票の生成、別表八(一)の非支配目的株式等(保有 5% 以下・益金不算入 20%)以外の区分、負債利子控除、みなし配当、短期保有株式の除外、計算期間が 12 か月を超える配当、過去年度への遡及である。このうち入力で検出できるものはエラーで止める。

# src/haito/models.py から抜粋
        if not (1 <= self.keisan_kikan_tsuki <= 12):
            raise ValueError("計算期間月数は1〜12")
        if self.hoyu_wariai is not None:
            # …
            if self.beppyo8_taisho and self.hoyu_wariai > Decimal("5"):
                raise ValueError(
                    "保有割合が5%超(非支配目的株式等でない)。非スコープのためエラー停止")

保有割合は任意の列なので、空欄の行はこの検査を通る。

申告書本体の帳票を扱う別の自作ツールとはリポジトリを分け、そちらの帳票には触れないことにした。リポジトリ間の import は禁止し、共有するのは規約(金額は int の円、丸めの集約、帳票構造は YAML を参照)と、マージ後の xtx を本体側で検証する運用だけである。

なお e-Tax は 2019 年のお知らせで、別表等(明細記載を要する部分)を XML 形式に加えて CSV 形式でも提出できるようにすると告知した。標準フォームの一覧(令和 5 年 4 月 1 日以後終了事業年度分)には別表六(一)・別表八(一)もある(2026-10-27 確認)。筆者は XML 片を xtx に差し込む方法しか検討しなかった。

2. 帳票の構造を一次資料に固定する

帳票の構造は推測で決めず、e-Tax の仕様書から起こした YAML を正とした。別表八(一)は手続内帳票対応表で帳票 ID(HOB800)と版を確定し、帳票フィールド仕様書と XSD から YAML を生成して、仕様書の版と出典を冒頭のコメントに残した。文書の記録では、検証用のスクリプトで YAML を XSD・Excel と突き合わせ、80 フィールドが一致した(本稿では再実行していない)。

別表八(一)は過去の xtx に現れず値を観測できなかったので、YAML を spec-only とし、観測済みの別表六(一)(本体側で検証済みの定義のコピー)と区別した。

別表八(一)を xtx の CONTENTS に差し込む位置を、当初の規則には「別表六(一)の後・別表七の前」と推測で書いていたが、手続内帳票対応表の帳票セット順では別表七系(HOB710〜HOB727)の後・別表十一系(HOC110)の前だったので、規則を書き換えた。

名前空間も XSD 検証で確かめた。和暦の日付の era・yy・mm・dd は general 名前空間、区分コード kubun_CD は XSD の restriction でローカルに宣言されているので帳票と同じ hojin 名前空間で出す。

3. 丸めを 1 か所に集め、様式の注記で確定する

金額は int の円で持ち、丸め・按分・割合は rounding.py に集めて直書きを禁止した。

個別法の控除所得税額には経路が 2 つある。所有期間割合を先に丸めて所得税額に掛ける「割合経由」と、所有月数 ÷ 計算期間月数をそのまま掛ける「分数直接」である。経路は METHOD(Literal["ratio", "fraction"] | None)で選び、契約を凍結するゲートで様式の注記を確かめるまでは None にした。None のままだと koujo_zei の最後の行(抜粋では省略)が例外で止めるので、未確定の経路で数字は出ない。

# src/haito/rounding.py から抜粋
def wariai_ceil3(shoyu_tsuki: int, kikan_tsuki: int) -> Decimal:
    # …
    return (Decimal(shoyu_tsuki) / Decimal(kikan_tsuki)).quantize(
        Decimal("0.001"), rounding=ROUND_CEILING)
# …
def koujo_zei(shotokuzei: int, shoyu_tsuki: int, kikan_tsuki: int) -> int:
    # …
    if METHOD == "ratio":
        return koujo_zei_ratio(shotokuzei, wariai_ceil3(shoyu_tsuki, kikan_tsuki))
    if METHOD == "fraction":
        return koujo_zei_fraction(shotokuzei, shoyu_tsuki, kikan_tsuki)
    # …
# …
def beppyo8_fusanyu(taisho_haito_gokei: int) -> int:
    # …
    return taisho_haito_gokei * 20 // 100

筆者が参照した国税庁「法人税のあらましと申告の手引」(令和 5 年版)の別表六(一)の頁にある様式には、所有期間割合(11)は小数点以下 3 位未満切上げ、控除を受ける所得税額(12)は (8)×(11) と注記されており(リポジトリの規則に残した要約)、割合経由に確定した。所得税額 536 円・所有 5 か月・計算期間 6 か月の例では、割合は 5/6 を切り上げた 0.834 で、分数直接より 1 円多くなる(次のテスト)。過去の申告で筆者が自作した出力は分数直接だったので、この差は「既知の許容差分」として 5 章のゴールデン比較より前に文書へ書いた。分数直接の関数は比較用に残し、本番の計算には使わない。

# tests/test_rounding.py から抜粋
    # 割合経由(記載要領の文言どおりならこちら): 536 × 0.834 = 447.024 → 447
    assert rounding.koujo_zei_ratio(536, Decimal("0.834")) == 447
    # 分数直接(過去の自作出力の実績・過少側): 536 × 5 / 6 = 446.67 → 446
    assert rounding.koujo_zei_fraction(536, 5, 6) == 446

別表八(一)の益金不算入額は、対象配当の合計に 20% を掛けて円未満を切り捨てる。丸めは合計に 1 回だけで、テストでは 10004 円が 2000 円になる。ただし、筆者が参照した同じ手引(令和 7 年版)の別表八(一)の頁には、この欄の端数処理を明示する文言が見当たらなかった(各様式の記載要領そのものを確かめた記録は無い)。筆者は法人税計算の一般原則(円未満切捨て)を運用上の前提として採用した。不算入額が小さくなる方向、つまり課税所得が増える側の判断であり、正しい扱いと確かめたものではない。

簡便法の(17)欄(期末と期首の所有元本数の差を 2 で割る)の切捨ても、明文が見当たらず採用した前提である。この欄は XSD の型が非負なので、元本が減ったときは 0 を出す。所有元本割合は 1 を上限にするので、控除額は変わらない。

4. 入力は契約で縛る

入力は CSV 1 本で、15 列の契約にした。Excel のテンプレートは openpyxl で作る生成物で、実行時は CSV しか読まない。合成データでは次の形になる(先頭の BOM は省いた)。

# tests/fixtures/synthetic/valid.csv から抜粋
銘柄コード,銘柄名,支払確定日,収入金額,源泉所得税額,計算方法,計算期間月数,所有期間月数,期首所有元本数,期末所有元本数,別表六対象,別表八対象,保有割合,基準日等,備考
9999,サンプル電機,2025-06-25,3500,536,個別,6,5,,,1,1,0.05,2025-03-31,
9998,テスト商事,2025-12-05,2000,306,簡便,12,,100,100,1,0,,,外国株想定
# …

1 行は pydantic のモデルで、strict・extra="forbid"・frozen にした。暗黙の型変換と知らない項目を拒み、計算方法によって必須の列と禁止の列が入れ替わる(個別法は所有期間月数が必須で期首・期末の元本数を禁止、簡便法はその逆)。読み込みは fail-fast にせず、ヘッダの完全一致を確かめたうえで全行を検証し、違反を行番号つきで列挙する。

個別法と簡便法の有利判定は別のツール(tools/compare_methods.py)で、控除所得税額計の大小だけで比べる。

契約で縛れるのは列の形と組み合わせまでで、値が事実に合うかは縛れない。期首・期末の元本数と所有期間月数は人が売買の記録から導いて入れるので、構想メモの記録では事実と矛盾する入力が実際に起きた。期中に取得したのに期首の元本数を入れた例などである。契約を先に固める進め方は契約ファーストで扱った。

5. 正しさを三層で守る

検証は三層にした。1 層目はゴールデンテストで、過去年の実申告の xtx から別表六(一)の全頁を抜き出した期待値(tools/extract_expected.py)と、同じ年のデータを CSV にして再生成した XML 片を、要素・属性・テキスト値で比べる。バイト一致は求めず、許容するのは丸め方式の変更による控除額の差だけである。

# tests/test_golden_hob016.py から抜粋
    for path, e, a in row_diffs:
        assert a - e == 1, f"{path}: 控除税額差分が+1円でない(expected={e} actual={a})"
    # 計欄差分 = 行差分の合計と厳密に一致すること
    delta = sum(a - e for _, e, a in row_diffs)
    for path, e, a in total_diffs:
        assert a - e == delta, f"{path}: 計欄差分 {a - e} が行差分合計 {delta} と不一致"

文書の記録では、丸め方式の変更による差分は 1 行の +1 円と、それを合計した計欄の +1 円だけで、ほかは要素・属性・値がすべて一致した。実データを含まないリポジトリの写しでは、26 件が通り、過去年の実データが要るゴールデンの 1 件はスキップした(2026-10-27 確認)。

2 層目は XSD 検証である。XML 片は単体だと IDREF の参照先(xtx の IT 部の ID)が無く検証が通らないので、様式の XSD を include し、ダミーの ID 要素と帳票グループの繰り返しを宣言したラッパースキーマを一時生成して検証する(tools/validate_fragment.py)。文書の記録では 2 枚とも通った(写しでは再実行していない)。マージ後の xtx を本体側のツールで構造検証することも計画ではこの層に含めていたが、後回しにしたまま、まだ行っていない。

3 層目は e-Tax ソフトへの組み込みで、表示とエラーチェックを見る。過去の観測が無い別表八(一)では、計画の文書でもここを実質のゴールデンと位置づけた。

6. xtx に差し込むときに壊したもの

マージのツール(tools/merge_xtx.py)の初版は、帳票コードの辞書順を保つ位置に挿入し、同じ帳票があれば置き換え、入力の xtx は上書きしない仕様だった。ただし実装は、xtx 全体を XML パーサ(ElementTree)でパースして書き出し直していた。

これを e-Tax ソフトに組み込むと、財務諸表の表示がエラーになった。xtx の CONTENTS には財務諸表の XBRL(XBRL2_1)が島のように入っており、書き出し直しでこの島の名前空間の接頭辞が付け替えられた。XBRL は xbrli:measure の値に iso4217:JPY のような QName(接頭辞つきの名前)を書く。ElementTree は要素名と属性名の接頭辞を振り直すが、テキストの中の接頭辞は文字列のまま残す、と筆者は理解している。結果として宣言と値の対応が切れた。

対応としては、木を書き出すのをやめ、バイト列を切り貼りする「テキストスプライス方式」に改めた。挿入・置換する帳票の範囲だけを書き換え、それ以外のバイト列は変えない。パースは検証にだけ使う。

# tools/merge_xtx.py から抜粋
def merge_form(data: bytes, pages: bytes, form: str) -> tuple[bytes, str]:
    starts = scan_forms(data)
    # テキスト走査とパース結果の突合(走査の誤検出=入れ子や属性値マッチを排除)
    if [c for _, c, _ in starts] != parsed_form_order(data):
        raise SystemExit(
            f"{form}: テキスト走査とXMLパースの帳票列が不一致(想定外の構造のため中断)")
    same = [(p, c, x) for p, c, x in starts if c == form]
    if same:
        b0, _ = page_span(data, *same[0])
        _, b1 = page_span(data, *same[-1])
        return data[:b0] + pages + data[b1:], "置換"
# …(以下は main() の、書き出す前の検証)
    # 検証2: XBRL2_1 島がバイト単位で不変(QName・接頭辞破壊の再発防止)
    xb_before, xb_after = xbrl_span(original), xbrl_span(data)
    if xb_before is not None and xb_before != xb_after:
        raise SystemExit("XBRL2_1 島が変化した(バイト保存の前提が破れている)")

回帰テストでは、接頭辞の宣言と iso4217:JPY を持つ XBRL の島を模した合成の xtx に 2 枚の XML 片をマージし、島がバイト単位で残ることを確かめる。

# tests/test_merge_xtx.py から抜粋
    # 回帰テスト: XBRL 島(接頭辞宣言・QName 値)がバイト単位で保存されること
    out_bytes = out.read_bytes()
    assert XBRL_ISLAND.encode("utf-8") in out_bytes
    assert b"<xbrli:measure>iso4217:JPY</xbrli:measure>" in out_bytes

挿入位置、再マージの置換、入力の上書きの拒否も同じファイルでテストした。筆者は改修後に再生成した xtx を手修正なしで提出し、申告を完了した(改修後の表示を e-Tax ソフトの画面で確かめた記録は無い)。

まとめ

2 枚の別表は丸めの集約・入力の契約・三層の検証で作り、不具合は自分で生成した帳票ではなく、差し込み先の xtx の触っていないはずの部分に出た。

契約・丸め・検証はコードと文書に残ったが、開発中の git 履歴は無く、申告の後に git での管理を始めた。最終のゲートの承認は申告を終えた後に記録し、個々の欄を画面で確かめた記録も無い。

筆者は次に変えることとして、git を最初から使う、ゲートの承認を承認した時点で記録する、人が導出する入力を減らす、実データを git 管理外の 1 か所にだけ置く規則をコミット前の確認で守る、の 4 つを考えている。次期は売買履歴から元本数と所有期間月数を導く Web ツールの構想がある。ゲートで区切る進め方はフェーズゲート制、確認をその時点で記録する方法は検証ログで扱った。

--- 参照: haito-forge のリポジトリ(README・CLAUDE.md・.claude/rules・docs・specs・src・tools・tests。2026-10-27 確認)。国税庁「法人税のあらましと申告の手引」令和 5 年版の別表六(一)の頁: https://www.nta.go.jp/publication/pamph/hojin/aramashi2023/pdf/02-10.pdf / 同 令和 7 年版の別表八(一)の頁: https://www.nta.go.jp/publication/pamph/hojin/aramashi2025/pdf/02-12.pdf / e-Tax のお知らせ(2019 年、別表等の CSV 形式での提出): https://www.e-tax.nta.go.jp/topics/2019/topics_oshirase_310228.htm / 標準フォームの一覧(令和 5 年 4 月 1 日以後終了事業年度分): https://www.e-tax.nta.go.jp/hojin/gimuka/csv_jyoho2_beppyo_05.htm (いずれも 2026-10-27 確認)。免責事項・出典と検証: https://www.doand.tech/disclaimer/