""" 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生成失敗")