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,
    }