h-1.flet.3/services/pdf_generator.py
2026-02-21 23:49:15 +09:00

246 lines
9.5 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:
# ファイル名生成ルール: {日付}({タイプ}){株式会社等を除く顧客名}_{件名or商品1行目}_{金額}円_{HASH下8桁}.PDF
date_str = invoice.date.strftime("%Y%m%d")
doc_type = invoice.document_type.value if invoice.document_type else "請求"
# 顧客名から株式会社等を除去
customer_name = invoice.customer.name
for suffix in ["株式会社", "有限会社", "合資会社", "合同会社"]:
customer_name = customer_name.replace(suffix, "")
# 顧客名から不正な文字を除去(ファイル名に使えない文字)
import re
customer_name = re.sub(r'[\\/:*?"<>|]', '', customer_name)
# 件名または商品1行目
subject_or_product = invoice.notes or ""
if not subject_or_product and invoice.items:
subject_or_product = invoice.items[0].description
# 件名から不正な文字を除去
subject_or_product = re.sub(r'[\\/:*?"<>|]', '', subject_or_product)
# 件名が長すぎる場合は短縮
if len(subject_or_product) > 30:
subject_or_product = subject_or_product[:30] + "..."
# 金額
total_amount = sum(item.subtotal for item in invoice.items)
amount_str = f"{total_amount:,}"
# ハッシュ(仮実装)
import hashlib
hash_input = f"{date_str}{doc_type}{customer_name}{subject_or_product}{total_amount}"
hash_value = hashlib.md5(hash_input.encode()).hexdigest()[:8]
filename = f"{date_str}({doc_type}){customer_name}_{subject_or_product}_{amount_str}_{hash_value}.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生成失敗")