Spaces:
Runtime error
Runtime error
File size: 9,816 Bytes
74728c6 2333bfb 1c4669e af926a9 2333bfb 1c4669e 74728c6 1c4669e 74728c6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 | """報表生成模組 - 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,
}
|