• #CF精算表
  • #Excel自動化
  • #openpyxl
  • #テストコード
  • #バグ修正
  • #INDEX/MATCH
開発eurekapu-nuxt4メモ

CF精算表のバグ修正とテストコード整備

CF精算表の自動生成スクリプトを回し始めると、Excelを開くたびに数字がずれている箇所が見つかった。1つ直すと別のセルが壊れる。モグラ叩きを繰り返しながらバグを潰し、最終的にcheck=0の数値検証テストを組んで「もう壊れたら気づける」状態まで持っていった一日の記録。

資本準備金が消えていた

BS階層を組み立てる gen_annual_table_core.py で、資本準備金 が個別行として出力されていなかった。Excelを開いて純資産の部を見ると、資本金 の下に来るはずの 資本準備金 がない。

原因は prune_empty_subtotals だった。この関数は値がゼロの小計行を間引く処理だが、資本準備金は小計ではなく末端科目なのに巻き込まれて落ちていた。修正として、株主資本合計 の数式に資本金系の科目を含めるよう書き換えた。

check行がCFWSに混入していた

年次推移表には各セクションの末尾に check 行がある。BSの借方合計と貸方合計の差額を検証するための行だ。この行がCF精算表のBS科目リストに紛れ込んでいた。CFWSのRow 10に「チェック」という行が表示され、数式が壊れていた。

parse_annual_data で年次推移表を読み取るとき、check行をスキップする条件を追加して解決した。

中間テーブルの正規化

借入金・貸付金・資本政策の3つの中間テーブルが、それぞれ別のレイアウトで組まれていた。列の並び順も参照の仕方もバラバラで、新しい勘定科目を追加するたびに3パターンの数式を書く必要があった。

これを「年度 x 勘定科目 x CF調整列名 -> 金額」の正規化形式に統一した。全テーブルで共通のINDEX/MATCH複合キーパターンを使う。

=INDEX(TPL_返済予定表!$L$18:$M$24,
  MATCH($C6&$C$2, TPL_返済予定表!$K$18:$K$24&TPL_返済予定表!$J$18:$J$24, 0),
  MATCH(L$3, TPL_返済予定表!$L$17:$M$17, 0))*-1

$C6&$C$2 で勘定科目と年度の複合キーを作り、中間テーブルのK列とJ列を結合した配列とMATCHする。この形なら、テーブルが変わっても参照先のシート名と範囲を差し替えるだけで済む。

中間テーブルの参照バグ

正規化した直後、N列(株式発行の調整列)がTPL_返済予定表を参照してしまうバグを踏んだ。返済予定表には株式発行のデータは存在しない。MATCHが#N/Aを返してセル全体がエラーになっていた。

テーブルに存在する (勘定科目, CF調整列名) ペアだけに数式を書くようスクリプトを修正した。存在しないペアにはIFERROR(..., 0)ではなく、そもそも数式を入れない方針にした。

@マーク問題(配列数式対応)

openpyxlが書いたINDEX/MATCHの数式をExcel 365で開くと、MATCHの引数に暗黙的交差演算子 @ が付加される現象に遭遇した。

MATCH(@$C6&@$C$2, ...)

@ が付くと配列評価が行われず、MATCHが単一セルしか見なくなる。複合キーの結合部分を INDEX(K列&J列,) でラップして配列評価を強制した。末尾の空カンマ ,) がポイントで、これによりINDEXが配列を返す式として評価される。

PL科目名の不一致

年次推移表では「受取利息」と表記されているが、CF精算表の調整辞書は「受取利息及び受取配当金」で検索していた。MATCHが#N/Aを返し、PL由来の調整項目が全滅していた。

adjustment dictに pl_name(年次推移表での科目名)を別フィールドとして保持するよう修正した。CF精算表上の表示名と、年次推移表での検索名を分離することで、表記揺れを吸収できるようになった。

check=0テストコードの追加

最初の失敗: 構造チェックしか書いていなかった

最初に書いたテストコードは「配分先が存在するか」だけを確認する構造チェックだった。数式が入っているかどうかは見るが、数式が正しい値を返すかは検証しない。

ユーザーから「Excelで数式が組めているのに、なぜテストコードで数値検証できない?」と指摘が飛んできた。確かにそのとおりで、openpyxlは数式の結果を計算しないが、中間テーブルの値はPythonで再計算できる。Excel上の数式と同じロジックをPythonで書けば、期待値は出せる。

数値検証版に書き換え

wb.save() の直前に以下の4つの検証を実行するテストコードを追加した。

  1. BS全年度バランス検証 --- 各年度の資産合計と負債純資産合計が一致するか
  2. 配分漏れチェック --- 全調整科目に数式または値が入っているか
  3. PL由来項目の存在チェック --- 受取利息、支払利息など必須のPL調整項目が欠落していないか
  4. 増減総和チェック --- BS増減の合計とCF調整列の合計が一致するか

トートロジーバグ

テストコードを回すと全項目check=0になった。一瞬喜んだが、DUAL/EQUITY科目のテストで alloc = h と書いていたことに気づいた。h は中間テーブルから取得した値ではなく、そのセル自身の値だった。つまり「自分自身と自分自身を比較してcheck=0」というトートロジーになっていた。

実際の中間テーブルの値をPythonで計算し直し、期待値として使うよう修正した。修正後に回すとcheck!=0の科目が2つ出てきて、テストがきちんと機能していることを確認できた。

TPL_回収予定表の文字列問題

スプレッドシートからエクスポートしたデータがopenpyxlで文字列としてコピーされていた。セルの値が "1000000" (文字列) であって 1000000 (数値) ではない。SUMIFSの条件に合致しても集計対象にならず、合計が0のままだった。

copy_sheet 関数に自動数値変換を追加した。セルの値が数値に変換可能な文字列であれば、コピー時にfloatへキャストする。

短期借入金が配分不能だった

Codexにスクリプトをレビューさせたところ、「短期借入金がDUAL_ACCOUNTSに定義されているのに、中間テーブルの参照先が長期借入金のテーブルに固定されている」と指摘が返ってきた。短期借入金は返済予定表の別セクションを参照する必要がある。中間テーブルの参照先を科目ごとに切り替えるよう修正した。

振り返り

今日の作業で一番手が止まったのはトートロジーバグの発見だった。テストが全部グリーンになった瞬間は気持ちよかったが、「なぜこんなに簡単に通るのか」と違和感を覚えて式を読み返したら、自分自身との比較だった。テストが通ること自体を疑う習慣がないと、偽の安心を積み上げてしまう。

もう1つの教訓は、科目名の表記揺れを甘く見ていたこと。「受取利息」と「受取利息及び受取配当金」の違いでMATCHが黙って#N/Aを返す。エラーにならずに0が入るだけなので、目視では気づきにくい。テストコードがなければ、おそらくしばらく気づかなかった。

check=0テストを入れたことで、今後新しい科目やテーブルを追加したときに壊れたら即座にわかる。モグラ叩きから、壊れたら警報が鳴る状態に移行できた。