h-1.flet.3/services/pdf_generator.py
2026-02-20 23:24:01 +09:00

214 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
PDF生成サービス
Flutter参考プロジェクトの機能をPythonで実装
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from datetime import datetime
from typing import Optional
from models.invoice_models import Invoice, InvoiceItem
import os
class PdfGenerator:
"""PDF生成サービス"""
def __init__(self):
self.output_dir = "generated_pdfs"
os.makedirs(self.output_dir, exist_ok=True)
# 日本語フォント登録(システムフォントを使用)
try:
pdfmetrics.registerFont(TTFont('Japanese', '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf'))
except:
# フォールバック:デフォルトフォント
pass
def generate_invoice_pdf(self, invoice: Invoice, company_info: dict) -> Optional[str]:
"""請求書PDFを生成
Args:
invoice: 伝票データモデル
company_info: 自社情報 dict{name, address, phone, registration_number}
Returns:
生成されたPDFファイルパス、失敗時はNone
"""
try:
# ファイル名生成: {会社ID}_{端末ID}_{連番}.pdf
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{company_info.get('id', 'COMP')}_{timestamp}.pdf"
filepath = os.path.join(self.output_dir, filename)
# PDF生成
c = canvas.Canvas(filepath, pagesize=A4)
width, height = A4
# 監査向けのメタ情報(バイナリ同一性は要求しない)
try:
c.setAuthor(company_info.get("id", ""))
c.setTitle(f"{invoice.document_type.value} {invoice.invoice_number}")
c.setSubject(getattr(invoice, "uuid", ""))
keywords = []
for key in ["payload_hash", "chain_hash", "node_id"]:
v = getattr(invoice, key, None)
if v:
keywords.append(f"{key}={v}")
if keywords:
c.setKeywords(",".join(keywords))
except Exception:
pass
# ヘッダー: 会社ロゴ・名前
self._draw_header(c, invoice, company_info, width, height)
# 顧客情報
self._draw_customer_info(c, invoice, width, height)
# 明細テーブル
self._draw_items_table(c, invoice, width, height)
# 合計金額
self._draw_totals(c, invoice, width, height)
# フッター: 登録番号など
self._draw_footer(c, company_info, width)
# payload_json をPDFに埋め込む不可視テキストとして格納
try:
payload_json = getattr(invoice, "payload_json", None)
if payload_json:
c.saveState()
c.setFillColorRGB(1, 1, 1)
c.setFont("Helvetica", 1)
# PDFのどこかに確実に残す見えない
c.drawString(1 * mm, 1 * mm, f"INVOICE_PAYLOAD_JSON:{payload_json}")
c.restoreState()
except Exception:
pass
c.save()
return filepath
except Exception as e:
print(f"PDF生成エラー: {e}")
return None
def _draw_header(self, c: canvas.Canvas, invoice: Invoice, company_info: dict, width: float, height: float):
"""ヘッダー描画"""
# 帳票種類タイトル
c.setFont("Helvetica-Bold", 24)
title = invoice.document_type.value
c.drawString(20*mm, height - 30*mm, title)
# 自社情報
c.setFont("Helvetica", 10)
company_name = company_info.get('name', '自社名未設定')
c.drawString(20*mm, height - 45*mm, f"{company_name}")
# 日付・番号
c.setFont("Helvetica", 10)
date_str = invoice.date.strftime("%Y年%m月%d")
c.drawString(width - 80*mm, height - 30*mm, f"発行日: {date_str}")
c.drawString(width - 80*mm, height - 40*mm, f"No. {invoice.invoice_number}")
def _draw_customer_info(self, c: canvas.Canvas, invoice: Invoice, width: float, height: float):
"""顧客情報描画"""
y_pos = height - 70*mm
c.setFont("Helvetica-Bold", 12)
c.drawString(20*mm, y_pos, "御中:")
c.setFont("Helvetica", 11)
customer_name = invoice.customer.formal_name
c.drawString(20*mm, y_pos - 8*mm, customer_name)
if invoice.customer.address:
c.setFont("Helvetica", 9)
c.drawString(20*mm, y_pos - 16*mm, invoice.customer.address)
def _draw_items_table(self, c: canvas.Canvas, invoice: Invoice, width: float, height: float):
"""明細テーブル描画"""
# テーブルヘッダー
y_start = height - 110*mm
col_x = [20*mm, 80*mm, 110*mm, 140*mm, 170*mm]
# ヘッダーライン
c.setFont("Helvetica-Bold", 10)
headers = ["品名", "数量", "単価", "金額", "摘要"]
for i, header in enumerate(headers):
c.drawString(col_x[i], y_start, header)
# 明細行
c.setFont("Helvetica", 9)
y_pos = y_start - 10*mm
for item in invoice.items:
c.drawString(col_x[0], y_pos, item.description[:20])
c.drawRightString(col_x[1] + 20*mm, y_pos, str(item.quantity))
c.drawRightString(col_x[2] + 20*mm, y_pos, f"¥{item.unit_price:,}")
c.drawRightString(col_x[3] + 20*mm, y_pos, f"¥{item.subtotal:,}")
y_pos -= 8*mm
def _draw_totals(self, c: canvas.Canvas, invoice: Invoice, width: float, height: float):
"""合計金額描画"""
y_pos = height - 180*mm
x_right = width - 40*mm
c.setFont("Helvetica", 10)
c.drawRightString(x_right, y_pos, f"小計: ¥{invoice.subtotal:,}")
c.drawRightString(x_right, y_pos - 8*mm, f"消費税: ¥{invoice.tax:,}")
c.setFont("Helvetica-Bold", 14)
c.drawRightString(x_right, y_pos - 20*mm, f"合計: ¥{invoice.total_amount:,}")
def _draw_footer(self, c: canvas.Canvas, company_info: dict, width: float):
"""フッター描画"""
y_pos = 30*mm
c.setFont("Helvetica", 8)
reg_num = company_info.get('registration_number', '')
if reg_num:
c.drawString(20*mm, y_pos, f"登録番号: {reg_num}")
# 支払期限・備考
c.drawString(20*mm, y_pos - 5*mm, "お支払期限: 月末まで")
# 使用例
if __name__ == "__main__":
from models.invoice_models import Customer, InvoiceItem, DocumentType
from datetime import datetime
# サンプルデータ
customer = Customer(1, "田中商事", "田中商事株式会社", "東京都千代田区", "03-1234-5678")
invoice = Invoice(
customer=customer,
date=datetime.now(),
items=[
InvoiceItem("商品A", 2, 15000),
InvoiceItem("商品B", 1, 30000),
],
document_type=DocumentType.INVOICE,
)
company = {
'id': 'SAMPLE001',
'name': 'サンプル株式会社',
'registration_number': 'T1234567890123'
}
generator = PdfGenerator()
pdf_path = generator.generate_invoice_pdf(invoice, company)
if pdf_path:
print(f"PDF生成成功: {pdf_path}")
else:
print("PDF生成失敗")