"""報表生成模組 - Report Generation Module. This module handles PDF and Excel report generation for the urban renewal project management system. """ import datetime import io import tempfile from pathlib import Path from typing import Any, Optional import pandas as pd from fpdf import FPDF # 使用系統中文字體 (macOS) SYSTEM_FONT_PATH = Path("/Library/Fonts/Arial Unicode.ttf") FONT_PATH = Path(__file__).parent / "kaiu.ttf" class FinanceReport(FPDF): """Custom PDF report class with Chinese font support.""" def __init__(self) -> None: """Initialize the PDF report with Chinese font.""" super().__init__() self._has_chinese_font = False # 優先嘗試系統字體 if SYSTEM_FONT_PATH.exists(): try: self.add_font("ArialUnicode", "", str(SYSTEM_FONT_PATH)) self._has_chinese_font = True self.font_name = "ArialUnicode" except Exception: pass # 其次嘗試專案字體 elif FONT_PATH.exists() and FONT_PATH.stat().st_size > 1000: try: self.add_font("kaiu", "", str(FONT_PATH)) self._has_chinese_font = True self.font_name = "kaiu" except Exception: pass self.add_page() self._set_default_font() def _set_default_font(self, size: int = 12) -> None: """Set the default font based on availability.""" if self._has_chinese_font: self.set_font(self.font_name, size=size) else: self.set_font("Helvetica", size=size) def header(self) -> None: """Add report header.""" if self._has_chinese_font: self.set_font(self.font_name, size=16) else: self.set_font("Helvetica", "B", 16) self.cell(0, 10, "Financial Report", ln=True, align="C") self.ln(5) def footer(self) -> None: """Add report footer with page number.""" self.set_y(-15) if self._has_chinese_font: self.set_font(self.font_name, size=8) else: self.set_font("Helvetica", "I", 8) self.cell(0, 10, f"Page {self.page_no()}", align="C") def add_section(self, title: str) -> None: """Add a section header. Args: title: Section title text. """ if self._has_chinese_font: self.set_font(self.font_name, size=14) else: self.set_font("Helvetica", "B", 14) self.ln(5) self.cell(0, 10, title, ln=True) self.ln(2) self._set_default_font(10) def add_metric(self, label: str, value: str) -> None: """Add a key-value metric line. Args: label: Metric label. value: Metric value. """ self.cell(60, 8, f"{label}:", border=0) self.cell(0, 8, value, border=0, ln=True) def add_table(self, df: pd.DataFrame, col_widths: Optional[list[int]] = None) -> None: """Add a styled table to the PDF.report. Args: df: DataFrame to render as table. col_widths: Optional list of column widths. """ if df.empty: self.cell(0, 8, "No Data", ln=True) return # 計算欄寬 if col_widths is None: col_widths = [int(190 / len(df.columns))] * len(df.columns) # 表頭 if self._has_chinese_font: self.set_font(self.font_name, size=9) else: self.set_font("Helvetica", "B", 9) for i, col in enumerate(df.columns): w = col_widths[i] if i < len(col_widths) else 30 self.cell(w, 7, str(col)[:15], border=1, align="C") self.ln() # 資料列 self._set_default_font(8) for _, row in df.head(20).iterrows(): # 限制最多 20 行 for i, val in enumerate(row): w = col_widths[i] if i < len(col_widths) else 30 text = str(val)[:20] if val is not None else "" self.cell(w, 6, text, border=1) self.ln() def generate_pdf_report( project_name: str, total_income: float, total_expense: float, profit: float, landowner_stats: dict[str, Any], milestone_summary: dict[str, Any], transactions_df: pd.DataFrame, stages_df: pd.DataFrame, risks_df: pd.DataFrame, ) -> bytes: """Generate a comprehensive PDF financial report. Args: project_name: Name of the project. total_income: Total income amount. total_expense: Total expense amount. profit: Net profit amount. landowner_stats: Landowner statistics dictionary. milestone_summary: Milestone summary dictionary. transactions_df: Financial transactions DataFrame. stages_df: Project stages DataFrame. risks_df: Risk events DataFrame. Returns: bytes: PDF file content as bytes. """ pdf = FinanceReport() # 報告資訊 pdf.add_section("報告資訊") pdf.add_metric("專案名稱", project_name) pdf.add_metric("生成日期", datetime.datetime.now().strftime("%Y-%m-%d %H:%M")) # 財務摘要 pdf.add_section("財務摘要") pdf.add_metric("總收入", f"{total_income:,.0f} 萬") pdf.add_metric("總支出", f"{total_expense:,.0f} 萬") pdf.add_metric("預計淨利", f"{profit:,.0f} 萬") pdf.add_metric("毛利率", f"{(profit / total_income * 100):.1f}%" if total_income > 0 else "N/A") # 地主同意狀況 pdf.add_section("地主同意狀況") pdf.add_metric("地主總數", f"{landowner_stats.get('total', 0)} 人") pdf.add_metric("同意率", f"{landowner_stats.get('agreement_rate', 0):.1f}%") pdf.add_metric("已簽署持分", f"{landowner_stats.get('agreed_share', 0):.1f}%") # 里程碑進度 pdf.add_section("里程碑進度") pdf.add_metric("總里程碑", f"{milestone_summary.get('total', 0)} 個") pdf.add_metric("已完成", f"{milestone_summary.get('completed', 0)} 個") pdf.add_metric("進行中", f"{milestone_summary.get('in_progress', 0)} 個") # 工程進度表 if not stages_df.empty: pdf.add_section("工程進度") pdf.add_table( stages_df[["工項名稱", "預算金額", "實際支出"]].head(10), col_widths=[60, 40, 40], ) # 風險事件 if not risks_df.empty: pdf.add_page() pdf.add_section("風險事件") pdf.add_table( risks_df[["風險類型", "描述", "發生機率", "影響程度"]].head(10), col_widths=[35, 70, 30, 30], ) return bytes(pdf.output()) def generate_excel_report( transactions_df: pd.DataFrame, stages_df: pd.DataFrame, landowners_df: pd.DataFrame, milestones_df: pd.DataFrame, risks_df: pd.DataFrame, loans_df: pd.DataFrame, inventory_df: pd.DataFrame, ) -> bytes: """Generate a comprehensive Excel report with multiple sheets. Args: transactions_df: Financial transactions data. stages_df: Project stages data. landowners_df: Landowner data. milestones_df: Milestone data. risks_df: Risk events data. loans_df: Loan contracts data. inventory_df: Inventory data. Returns: bytes: Excel file content as bytes. """ output = io.BytesIO() with pd.ExcelWriter(output, engine="xlsxwriter") as writer: # 現金流表 if not transactions_df.empty: pivot_df = transactions_df.pivot_table( index="月份", columns="科目名稱", values="金額", fill_value=0 ).reset_index() pivot_df.to_excel(writer, sheet_name="現金流", index=False) # 工程進度 if not stages_df.empty: stages_df.to_excel(writer, sheet_name="工程進度", index=False) # 地主名冊 if not landowners_df.empty: landowners_df.to_excel(writer, sheet_name="地主名冊", index=False) # 里程碑 if not milestones_df.empty: milestones_df.to_excel(writer, sheet_name="里程碑", index=False) # 融資合約 if not loans_df.empty: loans_df.to_excel(writer, sheet_name="融資合約", index=False) # 銷售庫存 if not inventory_df.empty: inventory_df.to_excel(writer, sheet_name="銷售庫存", index=False) # 風險事件 if not risks_df.empty: risks_df.to_excel(writer, sheet_name="風險事件", index=False) output.seek(0) return output.getvalue() def calculate_cashflow_summary(df: pd.DataFrame) -> dict[str, Any]: """Calculate cash flow summary statistics. Args: df: Financial transactions DataFrame. Returns: dict: Summary statistics including cumulative balance by month. """ if df.empty: return { "total_income": 0, "total_expense": 0, "net_profit": 0, "monthly_balance": [], } income = df[df["科目性質"] == "收入"]["金額"].sum() expense = df[df["科目性質"] == "支出"]["金額"].sum() # 計算每月累計餘額 monthly = df.groupby("月份").apply( lambda x: x[x["科目性質"] == "收入"]["金額"].sum() - x[x["科目性質"] == "支出"]["金額"].sum() ) cumulative = monthly.cumsum() return { "total_income": income, "total_expense": expense, "net_profit": income - expense, "monthly_balance": cumulative.tolist(), "min_balance": cumulative.min(), "min_balance_month": cumulative.idxmin() if len(cumulative) > 0 else 0, }