Spaces:
Runtime error
Runtime error
| """報表生成模組 - 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, | |
| } | |