LibreOffice Calc で Python から帳票作成(シートのスタイル以外は)

Excel から LibreOffice Calc に移行するときに、いわゆる帳票の作成ができるか否か私的には結構な肝になります。ちまちまと Calc で罫線を引いているのならば別なのですが、プログラムを使い自動化をしているとちょっと状況が異なります。

ClosedXMLを使って、超高速にリスト形式の帳票を作成する https://www.moonmile.net/blog/archives/9672

自分が良く使っているのは(これは逆引き大全にも入っているけど)、こんな風に C# + ClosedXML で帳票を自動作成するパターンです。帳票を作るツールはいくつかあるのですが、この場合は帳票のテンプレートをカスタムしやすくするように、テンプレートを Excel 自身で作成しています。罫線やら印刷時のタイトルなどはテンプレートとなる Excel ファイルに書いておきます。そして、シートに数値や文字列だけを C# から書き込むようにします。

このようにすると、

  • 年次のバージョンアップのように、帳票が変化してもプログラムに手を入れなくて済む
  • プログラムをつくるときにも罫線の修正や出力位置は Excel を使うことができるので、帳票政策が楽

と言う利点があります。欠点としては

  • Excel を業務 PC に入れないといけない

ですね。当然といえば当然なのですが、C# から Excel オブジェクトを扱うために(実際には ClosedXML なので、OpenXML のほうなのですが)Excel 自体が必須になってしまいます。

実は、ファイル作成だけならば Excel は必要がないのですが、印刷時に Excel オブジェクトが必要なのと、帳票のシート自体の編集に Excel が必要なので、開発者はともかくとしてお客にも Excel が必要になってしまいます。まあ、大抵は Excel を持っているからいいんですが。

Excel + C# + ClosedXML の組み合わせを LibreOffice Calc に変換できるか?

肝は、Excel + ClosedXML の組み合わせが、LibreOffice Calc + UNO API でできるか?ということです。私としては C# から UNO API を使えればよいのですが、前記事から C# から UNO API を使うには難点があるので、素直に Python でコーディングをします。

まあ、コーディングをするといってもベースを作るのは AI エージェントなので、それを動かしてちょこちょこと Python コードを修正することになります。

帳票テンプレート

まず、帳票テンプレート(report-template.ods)を用意します。

テンプレートのシートには、罫線やタイトル行の色を付けておきます。他にも行の高さも変えます。セルのフォーマットも変えておきましょう。実際の帳票を作るように試してみるのがよいです。

印刷時のヘッダーやフッター、タイトル行なんかも入れておくとよいです。

このテンプレートのシートは、作成するシートとしてコピーされてるので、このような印刷フォーマットなども使えます。先の C# + ClosedXML の例もそうなのですが、この手の細かい調節をプログラムですべてやるのは大変です。複雑な処理をしたい場合は、別途帳票ツールを購入したほうが良いでしょう。ただし、そこまで複雑すぎないとか、年単位で帳票のフォーマットが変わってしまうとか、そういうカスタムが多い場合は、こんな形で Excel あるいは Office Calc でテンプレートとなるシートを作ったほうが楽です。

実際、先の業務では20帳票位のテンプレートを Excel で自作しています。その分、プログラムコードが激減するのです。

AI エージェントに渡すプロンプト

実装するためのプロンプトは以下の通りです。


"""
テンプレートファイルを使って Calc レポートを作成するサンプル

# 実装手順

1. テンプレートファイルを開く
2. テンプレートシートをコピーして新しいシートを作成
3. サンプルデータの行数を計算する
4. シートの1行目はヘッダー行なのでそのまま
5. 2行目以降にサンプルデータをデータの行数分だけ用意する。
 5.1 行の罫線も一緒にコピーして、行数を増やす
6. データを行数分書き込む

"""

何等かのシステムならば色々目的なり設計を書いたほうがいいのですが、今回は既に作成されている Excel + ClosedXML のコードを模倣したいので、その手順を直接書いています。C# のコードを渡して「UNO API + Python に書き直して」でも通じるかもしれませんが、今回は少し丁寧に手順を書いています。業務コードのほうは、業務特有の複雑なパターンが掛かれているので、余計な情報を詰め込まないといけないので、ここではサンプル帳票にあわせます。

できあがったコード

少し手直しをしていますが、ほぼそのまま AI エージェント(GPT-5.1-Codex-Max)で書いたものです。行の高さが上手くコピーできていなかったので、後からプロンプトで足しています。

import uno
from pathlib import Path

# サーバーの起動
# & "C:\Program Files\LibreOffice\program\soffice" --accept="socket,host=localhost,port=2002;urp;" --norestore --nologo
# PowerShell での起動
# & "C:\Program Files\LibreOffice\program\python" .\calcReport001.py

"""
テンプレートファイルを使って Calc レポートを作成するサンプル

# 実装手順

1. テンプレートファイルを開く
2. テンプレートシートをコピーして新しいシートを作成
3. サンプルデータの行数を計算する
4. シートの1行目はヘッダー行なのでそのまま
5. 2行目以降にサンプルデータをデータの行数分だけ用意する。
 5.1 行の罫線も一緒にコピーして、行数を増やす
6. データを行数分書き込む

"""


# 設定: テンプレートと出力先
TEMPLATE_NAME = "report-template.ods"
OUTPUT_NAME = "report-output.ods"
TEMPLATE_SHEET_NAME = "Template"
REPORT_SHEET_NAME = "Report"


def to_file_url(path: Path) -> str:
	return uno.systemPathToFileUrl(str(path.resolve()))


def connect_desktop():
	local_ctx = uno.getComponentContext()
	resolver = local_ctx.ServiceManager.createInstanceWithContext(
		"com.sun.star.bridge.UnoUrlResolver", local_ctx
	)
	ctx = resolver.resolve(
		"uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext"
	)
	smgr = ctx.ServiceManager
	desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
	doc = desktop.getCurrentComponent()
	if doc is None:
		raise RuntimeError("アクティブな Calc ドキュメントが見つかりません")
	return desktop, doc


def load_template(desktop, template_path: Path):
	url = to_file_url(template_path)
	props = (uno.createUnoStruct("com.sun.star.beans.PropertyValue"),)
	props[0].Name = "Hidden"
	props[0].Value = True
	return desktop.loadComponentFromURL(url, "_blank", 0, props)


def import_template_sheet(target_doc, template_doc, template_sheet_name: str, report_sheet_name: str):
	target_sheets = target_doc.Sheets
	if target_sheets.hasByName(report_sheet_name):
		target_sheets.removeByName(report_sheet_name)

	# importSheet: 第3引数は挿入位置(末尾に追加)
	target_sheets.importSheet(template_doc, template_sheet_name, target_sheets.getCount())

	# 取り込んだシート名はテンプレートのものなので、目的の名前に変更
	imported_sheet = target_sheets.getByName(template_sheet_name)
	imported_sheet.Name = report_sheet_name
	return imported_sheet


def ensure_rows_with_format(sheet, template_row_idx: int, start_row: int, rows_needed: int, last_col: int):
	template_range = sheet.getCellRangeByPosition(0, template_row_idx, last_col, template_row_idx)
	source_addr = template_range.getRangeAddress()
	template_height = sheet.getRows().getByIndex(template_row_idx).Height

	for i in range(rows_needed):
		target_row = start_row + i
		if target_row == template_row_idx:
			continue  # テンプレート行は既に存在
		dest_addr = uno.createUnoStruct("com.sun.star.table.CellAddress")
		dest_addr.Sheet = source_addr.Sheet
		dest_addr.Column = 0
		dest_addr.Row = target_row
		sheet.copyRange(dest_addr, source_addr)
		sheet.getRows().getByIndex(target_row).Height = template_height


def write_data(sheet, data, start_row: int = 1, start_col: int = 0):
	for r, row in enumerate(data):
		for c, value in enumerate(row):
			cell = sheet.getCellByPosition(start_col + c, start_row + r)
			if isinstance(value, (int, float)):
				cell.setValue(value)
			else:
				cell.setString(str(value))


def main():
	base_dir = Path(__file__).resolve().parent
	template_path = base_dir / TEMPLATE_NAME
	output_path = base_dir / OUTPUT_NAME

	sample_data = [
		["001", "Alice", "Sales", 1200.5],
		["002", "Bob", "Marketing", 980.0],
		["003", "Carol", "Engineering", 1540.75],
		["004", "Dave", "Support", 760.25],
	]

	if not template_path.exists():
		raise FileNotFoundError(f"テンプレートが見つかりません: {template_path}")

	desktop, active_doc = connect_desktop()
	template_doc = load_template(desktop, template_path)

	# テンプレートドキュメントからシートをアクティブドキュメントへコピー
	report_sheet = import_template_sheet(
		target_doc=active_doc,
		template_doc=template_doc,
		template_sheet_name=TEMPLATE_SHEET_NAME,
		report_sheet_name=REPORT_SHEET_NAME,
	)

	# データ件数に合わせて行を用意(ヘッダー1行 + データ行)
	last_col = len(sample_data[0]) - 1 if sample_data else 0
	ensure_rows_with_format(
		sheet=report_sheet,
		template_row_idx=1,  # テンプレートの明細行(2行目)
		start_row=1,
		rows_needed=len(sample_data),
		last_col=last_col,
	)

	# データを書き込み(1行目はヘッダー想定)
	write_data(report_sheet, sample_data, start_row=1, start_col=0)

	# 別名で保存
	store_props = (uno.createUnoStruct("com.sun.star.beans.PropertyValue"),)
	store_props[0].Name = "FilterName"
	store_props[0].Value = "calc8"
	active_doc.storeToURL(to_file_url(output_path), store_props)
	print(f"レポートを保存しました: {output_path}")

	# 後処理: そのまま開いたままでも良いが、ここではクローズ
	template_doc.dispose()


if __name__ == "__main__":
	try:
		main()
	except Exception as e:
		print(f"error: {e}")


書き込むデータが sample_data として直書きになっていますが、これは SQLite や CSV ファイルなどから読み込むように変更すればいいでしょう。別途 Calc のシートにあるデータを参照してもよいかもしれません。別シートのデータを参照するのは、請求書などの1枚帳票でやることが多いので、後で試してみましょう。

動作確認

Python で実行させるとこんな感じです。現在開いている Calc に新しいシート「Report」が追加されます。ここでは、Calc を表示した状態にしていますが、自動でファイルに保存することができます。もともと、AI エージェントのコードは画面に表示せず、ファイルに保存するだけのものになっています。

ここでは、確認の意味もあって、Calc を表示させたままにしています。

印刷プレビューの状態

シート自体はうまくいっているのですが、印刷時のスタイルがうまくいかないようです。
どうやら、LibreOffice Calc では、シートをUIでドラッグ&ドロップしたときでもシート単位でのスタイルが設定されないようで、これは仕様みたいですね。


def import_template_sheet(target_doc, template_doc, template_sheet_name: str, report_sheet_name: str):
	target_sheets = target_doc.Sheets
	if target_sheets.hasByName(report_sheet_name):
		target_sheets.removeByName(report_sheet_name)

	# importSheet: 第3引数は挿入位置(末尾に追加)
	target_sheets.importSheet(template_doc, template_sheet_name, target_sheets.getCount())

	# 取り込んだシート名はテンプレートのものなので、目的の名前に変更
	imported_sheet = target_sheets.getByName(template_sheet_name)
	imported_sheet.Name = report_sheet_name

	# ページスタイル(印刷の向きやヘッダー/フッター等)をプロパティ単位でコピー
	template_styles = template_doc.getStyleFamilies().getByName("PageStyles")
	target_styles = target_doc.getStyleFamilies().getByName("PageStyles")
	page_style_name = imported_sheet.PageStyle

	if page_style_name:
		template_style = template_styles.getByName(page_style_name)
		if not target_styles.hasByName(page_style_name):
			new_style = target_doc.createInstance("com.sun.star.style.PageStyle")
			target_styles.insertByName(page_style_name, new_style)
		target_style = target_styles.getByName(page_style_name)

		# よく使うプロパティを安全に転写
		for prop in [
			"IsLandscape",
			"HeaderIsOn",
			"FooterIsOn",
			"PrintHeaders",
			"PrintGrid",
			"LeftMargin",
			"RightMargin",
			"TopMargin",
			"BottomMargin",
			"ScaleToPages",
			"ScaleToPagesX",
			"ScaleToPagesY",
		]:
			try:
				setattr(target_style, prop, getattr(template_style, prop))
			except Exception:
				pass

		imported_sheet.PageStyle = page_style_name
	return imported_sheet

試しにスタイルを手動でコピーするコードを入れてみたのですがうまくいきません。

【LibreOffice/Basic】Calcでアクティブシートにページスタイル(ページ設定)を追加するマクロ #LibreOfficeBasic – Qiita https://qiita.com/brunojacopteteth/items/23f614afc6b1b929c297

Basic のマクロならばできるようなので、「標準」ではなくて、新しいスタイルシートを作成して追加すればよさそうです。

「書式」→「ページスタイル」では、ページスタイルを編集できないっぽいので、マクロ専用なのか、それとも何か別な方法があるのか?

このあたりは、もうちょっと調べる必要あり。

カテゴリー: 開発, LibreOffice パーマリンク