• #CFWS
  • #Excel
  • #HTML
  • #iframe
  • #postMessage
  • #Python
  • #openpyxl
開発eurekapu-nuxt4メモ

この日やったこと

CF精算表(CFWS)のExcelシートをHTMLテーブルに変換するビューアを組み上げた。openpyxlで数式セルが空を返す壁にぶつかり、Excel COM経由で計算済みファイルを生成する迂回路を掘った。iframeの高さが足りず下半分が切れる問題は、postMessageベースの高さ通知で片づけた。最後にcheck行の値を拾ってタブを赤く染める仕組みを入れ、浮かび上がった5件のバグのうち4件を修正した。


Excel→HTML変換: 数式セルが空を返す

data_only=Falseの罠

xlsx_to_html.pyでExcelファイルを読み込み、HTMLテーブルに変換するスクリプトを書いた。最初はdata_only=Falseで開いたため、数式が入ったセルは数式文字列そのものが返り、計算結果は空のまま表示された。テーブルの半分以上が空白で埋まり、一目で何かがおかしいとわかった。

data_only=Trueに切り替えると、最後にExcelアプリで開いてキャッシュされた値が返る。ただしopenpyxlは数式エンジンを持たないため、一度もExcelで開かれていないファイルや、数式の依存チェーンが深いセルではNoneが返る。

Excel COM経由で計算済みファイルを生成

openpyxl単体では数式連鎖を解決できない。そこでExcel COMオートメーション(win32com.client)を使い、Excelアプリにファイルを開かせて全数式を再計算させ、計算済みの値をxlsx形式で別名保存するスクリプトを追加した。この計算済みファイルをopenpyxlのdata_only=Trueで読めば、全セルに値が入る。

IBフォーマットの色分け再現

CFWSはIBフォーマット(投資銀行スタイル)で色分けされている。青文字はハードコード値、黒文字は数式。openpyxlでセルのフォントカラーを読み取り、HTMLの<td>にインラインスタイルとして反映した。数式セルと手入力セルの区別がブラウザ上でもそのまま見える。


iframeリサイズ: 3つの壁を越える

壁1: 固定高さで下半分が切れる

HTMLテーブルをiframeで埋め込んだが、height: 600pxの固定値ではシートの行数によって下半分が切れた。シートごとに行数が違うため、固定値では対応できない。

壁2: file://のクロスオリジン制限

contentDocumentでiframe内のDOMを取得してscrollHeightを読もうとしたが、file://プロトコルではクロスオリジン制限に引っかかってnullが返った。ローカル開発中のHTMLファイルをfile://で開いている以上、この制限は回避できない。

寄り道: インラインテーブル方式

iframeをやめてHTMLテーブルを親ページに直接埋め込む方式に切り替えた。高さ問題は消えたが、各シートのHTMLが独立ファイルとして存在しなくなり、個別のデバッグや差し替えがやりにくくなった。ファイル独立性を保ちたいのでiframeに戻した。

解決: postMessageベースの高さ通知

iframe内のHTMLに小さなスクリプトを埋め込み、読み込み完了時にpostMessageで自身のscrollHeightを親ウィンドウに送る。親側はmessageイベントを受け取ってiframeのheight属性を動的に書き換える。

// iframe内(各シートHTML末尾)
window.addEventListener('load', () => {
  parent.postMessage(
    { type: 'cfws-resize', height: document.body.scrollHeight },
    '*'
  );
});
// 親ページ
window.addEventListener('message', (e) => {
  if (e.data.type === 'cfws-resize') {
    iframe.style.height = e.data.height + 'px';
  }
});

非表示タブのscrollHeight=0問題

タブ切り替えUIを実装した後、非アクティブタブのiframeはdisplay: noneになるためscrollHeightが0を返す。タブを切り替えた瞬間にiframeが表示されるが、高さが0のまま潰れていた。

対処として、タブ切り替え時に親からcfws-request-heightメッセージをiframeに送り、iframe側が再度scrollHeightを計測して返す仕組みを追加した。

// 親 → iframe: 高さ再計測を要求
iframe.contentWindow.postMessage({ type: 'cfws-request-height' }, '*');

これで、どのタブに切り替えても正しい高さでiframeが展開される。


Excelビューア機能の追加

行番号・列ヘッダー

HTMLテーブルの左端に行番号(1, 2, 3...)、上端に列ヘッダー(A, B, C...)を追加した。Excelのシートを眺めている感覚にそのまま近づけるためで、セル位置の特定が速くなった。

列ヘッダーにはmax-widthを設定して長い列名が折り返し表示されるようにした。ただし最終列だけは制限なしにして、端が見切れないようにした。

check!=0タブの赤色可視化

CFWSにはcheck行があり、値が0でなければどこかの計算が合っていない。この情報をタブUIに反映した。

最初はHTMLテーブルの全行をスキャンして「Check」というテキストを含む行を探していたが、黄色背景のセル(Checkセル固有のスタイル)を直接検索するロジックに切り替えた。テキスト検索よりも誤検出が減り、確実にcheck行だけを拾える。

check!=0のタブにはcheck-ngクラスを付与し、CSSで赤い背景色とツールチップを表示する。タブ一覧を見るだけで「どのシートの計算が合っていないか」が目に飛び込んでくる。


check!=0から浮かび上がった5件のバグ

赤く染まったタブを順に開いて、原因を掘り下げた。

Q3-6: 法人税等の支払額が二重計上

営業CFの小計を計算するop_subtotal_itemsに「法人税等の支払額」が含まれていた。法人税は営業CF小計の外(小計の下)に表示する項目なので、小計に含めると二重にカウントされる。op_subtotal_itemsから除外して解決。

Q3-7 / Q4-2: 繰越利益剰余金のNI列に配当金が混入

繰越利益剰余金の当期純利益(NI)列に、配当金のalloc値が紛れ込んでいた。NI列には当期純利益の残額だけを計上すべきで、配当金の配分値は別の列に入る。alloc計算のフィルタリング条件を修正し、NI列には残額のみが入るようにした。

Q3-7は修正完了。Q4-2は連結仕訳が絡むため、構造の見直しが必要で保留にした。

Q5-3: _TOPIC_CF_MAPのNone値がフォールスルー

_TOPIC_CF_MAP辞書でキーが存在するが値がNoneのエントリがあった。dict.get(key, default)は「キーが存在しない場合」にデフォルト値を返すが、「キーが存在して値がNone」の場合はそのままNoneを返す。

# 修正前: Noneがフォールスルーする
cf_item = _TOPIC_CF_MAP.get(topic, fallback)

# 修正後: キー存在 + 値チェック
cf_item = _TOPIC_CF_MAP.get(topic)
if cf_item is None:
    cf_item = fallback

これでNone値のエントリを正しくフォールバックに回せるようになった。

修正スコア: 4/5

5件中4件を修正完了。残り1件(Q4-2: 連結仕訳の繰越利益剰余金)は連結精算表側の構造に踏み込む必要があり、別タスクとして切り出した。


学んだこと

  • openpyxlは数式エンジンを持たない。数式の依存チェーンを解決するにはExcel COMで一度開いて再計算させる必要がある。このステップを最初にやっておけば、data_onlyの罠にハマる時間を節約できた
  • iframeの高さ自動調整はpostMessageが鉄板。contentDocumentはクロスオリジンで塞がれる場面が多い
  • dict.get(key, default)はキー不在にしか効かない。値がNoneのケースは別途チェックが要る。Pythonの基本だが、辞書のエントリ数が多いと見落とす
  • check行の可視化をタブUIに組み込んだことで、5件のバグが一目で浮かび上がった。手動でシートを1枚ずつ開いて確認していた頃と比べると、発見までの時間が段違いに縮まった