LibreOffice Calc + Python で単票(請求書)を作成

手もとにある Excel で作成した請求書を LibreOffice Calc に変換していきます。

請求書のような単票は、Word で作るのが普通だと思うのですが、Excel のほうが意外と楽だったりします。まあ、C# で単票を操作したときに Word よりも Excel でレイアウトを作ったほうが楽だったという理由と、Excel/Word の両方に対応するのは面倒だったというのも大きな理由ですが、

  • 数値や文字列などを隠しデータシートに書き込んでおく
  • 印刷用のシートからデータシートを参照させる

の2手順にしておくとプログラムが楽になります。
いわゆる、プログラムから書き込むセルの位置を固定にできる利点があります。単票のレイアウトが変わったときは、データシートはそのままの形式にして、印刷用のシートを変更すればよいのです。

単票のテンプレート

印刷用のシートを作成しておきます。

データシートはこんな感じです。印刷用のシートから “=$Data.B1” のように参照します。

印刷プレビューはこんな感じ。

ひとつ注意しないといけないのは、セルにファンクションを入れると「0」が出てしまうことがあります。これは Excel でも LibreOffice Calc でも、こんな感じで「ゼロ値」のチェックを外すと表示が消えるようになります。

で、Excel の場合はこれで消えるのですが、LibreOffice Calc ではバグなのか仕様なのか、印刷するときや PDF に出力するときには「0」が出てしまいます。これ、きっとバグなんですが…仕方がないので、書式で設定します。

これが結構面倒で、文字列の場合と金額の場合は書き分けないといけません。

文字列の場合
0;-0;;@
数値の場合
[>0]0;[<0]-0;"";@
金額の場合
[>0]"\"#,##0;[<0][RED]"\-"#,##0;"";@

「0」が表示される場合は、こんな感じになります。

これ「ゼロ値」外せば大丈夫なときと駄目なときがあるので、LibreOffice のバージョンで確認してください。ちょっと不安定っぽいです。

Python コード

以下のコードで請求書の Calc ファイルが生成されるようになります。

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" .\calcBill001.py

"""
請求テンプレート(.ots)をテンプレートとして新規ドキュメントで開き、Dataシートに書き込んで請求書を生成するサンプル。

手順:
1. 「請求書テンプレート」(bill-template.ots) をテンプレートとして新規ドキュメントを開く
2. シート名「Data」にサンプルデータを書き込む(B1:宛名, B2:件名 など固定セルに投入)
3. 明細行(任意で複数)を書き込む。必要ならテンプレートの行書式を複製してからデータ投入。
4. 別名で保存
"""


# 設定
TEMPLATE_NAME = "bill-template.ots"
OUTPUT_NAME = "bill-output.ods"
DATA_SHEET_NAME = "Data"
ITEM_START_ROW = 5  # 明細の開始行(0-based, 10行目). テンプレート構成に合わせて調整
ITEM_TEMPLATE_ROW = 8  # 書式をコピーする元行(0-based). 明細の1行目など。


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 create_doc_from_template(desktop, template_path: Path):
	url = to_file_url(template_path)
	props = [
		uno.createUnoStruct("com.sun.star.beans.PropertyValue"),
		uno.createUnoStruct("com.sun.star.beans.PropertyValue"),
	]
	props[0].Name = "AsTemplate"
	props[0].Value = True
	props[1].Name = "Hidden"  # UIが欲しい場合は False に変更
	props[1].Value = True
	return desktop.loadComponentFromURL(url, "_blank", 0, tuple(props))


def set_scalar(sheet, address: str, value):
	try:
		sheet.getCellRangeByName(address).setString(str(value))
	except Exception:
		pass

def write_items(sheet, items, start_row: int, start_col: int = 0):
	"""明細行を書き込む。 items は2次元配列。"""
	for r, row in enumerate(items):
		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

	# サンプル固定項目
	fields = {
		"B1": "株式会社サンプル 様",  # 宛名
		"B2": "12月分 請求書",       # 件名
		"B3": "2512-01",        # 請求番号
		"B4": "2025/12/17",        # 請求日
		"B18": "これはサンプル請求書です",  # 備考
	}

	# 明細: [No, 摘要, 数量, 単位, 単価]
	items = [
		[1, "商品A", 2, "個", 1500],
        [2, "商品B", 1, "式", 3000],
        [3, "商品C", 5, "個", 800],
		[4, "商品D", 3, "個", 1200],
	]

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

	desktop, _ = connect_desktop()
	doc = create_doc_from_template(desktop, template_path)
	sheets = doc.Sheets

	if not sheets.hasByName(DATA_SHEET_NAME):
		raise RuntimeError("テンプレートに Data シートがありません")
	data_sheet = sheets.getByName(DATA_SHEET_NAME)

	# 固定セルを書き込み
	for addr, val in fields.items():
		set_scalar(data_sheet, addr, val)

	write_items(data_sheet, items, start_row=ITEM_START_ROW, start_col=1)

	# 保存
	store_props = (uno.createUnoStruct("com.sun.star.beans.PropertyValue"),)
	store_props[0].Name = "FilterName"
	store_props[0].Value = "calc8"
	doc.storeToURL(to_file_url(output_path), store_props)
	print(f"請求書を保存しました: {output_path}")

	# 後処理
	try:
		doc.dispose()
	except Exception:
		pass


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


これでいったん、表形式の帳票と単票の作成ができることが確認できました。

自分が業務で使っているパターンとしては網羅できたので、あとは使い勝手です。これらの Python コードはほとんどが AI エージェントで作成して貰っているので、手が出しずらい状態になっています。あくまで使い捨てのコーディングになっているので、細かいところを修正しようとするとプロンプトから手をいれることになってしまいます。

  • せめて Calc を操作するところの UNO API のコード補完が欲しい → スタブの作成
  • 構造体の作成で createUnoStruct が必要になるので、これを適当にラップする
  • getCellByPosition を Cells に変えるとか、VBA に寄せた命名にしたい

このあたりは、年明けぐらいに試してみる予定。

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