Spaces:
Runtime error
Runtime error
| """SPV 都更專案資料庫模組 - Database Module for Urban Renewal SPV Management. | |
| This module handles all database operations including table creation, | |
| data loading, and CRUD operations for the urban renewal project management system. | |
| """ | |
| import datetime | |
| import os | |
| import shutil | |
| import sqlite3 | |
| from typing import Any, Optional | |
| import pandas as pd | |
| DB_PATH = "spv_management.db" | |
| def get_connection() -> sqlite3.Connection: | |
| """Get a database connection with row factory enabled. | |
| Returns: | |
| sqlite3.Connection: Database connection object. | |
| """ | |
| conn = sqlite3.connect(DB_PATH) | |
| conn.row_factory = sqlite3.Row | |
| return conn | |
| def init_db() -> None: | |
| """Initialize the database with all required tables. | |
| Creates tables for: | |
| - Finance transactions (財務交易) | |
| - Project parameters (專案參數) | |
| - Project stages (工程階段) | |
| - Loan contracts (融資合約) | |
| - Inventory (銷售庫存) | |
| - Landowners (地主管理) - NEW | |
| - Milestones (里程碑) - NEW | |
| - Risk events (風險事件) - NEW | |
| - Scenarios (情境模擬) - NEW | |
| """ | |
| conn = get_connection() | |
| c = conn.cursor() | |
| # 核心財務表:交易紀錄制,支援無限擴充自訂科目 | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS finance_transactions ( | |
| 月份 INTEGER, | |
| 類型 TEXT, | |
| 科目名稱 TEXT, | |
| 金額 REAL, | |
| 科目性質 TEXT, | |
| PRIMARY KEY (月份, 類型, 科目名稱) | |
| ) | |
| """) | |
| # 專案參數 | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS project_params ( | |
| 參數名稱 TEXT PRIMARY KEY, | |
| 數值 REAL | |
| ) | |
| """) | |
| # 工程階段 | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS project_stages ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 工項名稱 TEXT, | |
| 開始月份 INTEGER, | |
| 持續月份 INTEGER, | |
| 成本佔比 REAL, | |
| 撥款佔比 REAL, | |
| 預算金額 REAL DEFAULT 0, | |
| 實際支出 REAL DEFAULT 0 | |
| ) | |
| """) | |
| # 融資合約 | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS loan_contracts ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 貸款名稱 TEXT, | |
| 授信額度 REAL, | |
| 年利率 REAL, | |
| 狀態 TEXT, | |
| 動撥金額 REAL DEFAULT 0, | |
| 到期日 DATE, | |
| 備註 TEXT | |
| ) | |
| """) | |
| # 銷售庫存 | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS inventory ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 物件名稱 TEXT, | |
| 預計售價 REAL, | |
| 總數量 INTEGER, | |
| 地主分回數量 INTEGER, | |
| 已售數量 INTEGER DEFAULT 0, | |
| 銷售狀態 TEXT | |
| ) | |
| """) | |
| # 地主管理 (NEW) | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS landowners ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 姓名 TEXT NOT NULL, | |
| 持分比例 REAL, | |
| 分回坪數 REAL, | |
| 分回戶型 TEXT, | |
| 同意書狀態 TEXT DEFAULT '待聯繫', | |
| 聯絡電話 TEXT, | |
| 地址 TEXT, | |
| 備註 TEXT, | |
| 最後聯繫日期 DATE | |
| ) | |
| """) | |
| # 里程碑 (NEW) | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS milestones ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 里程碑名稱 TEXT NOT NULL, | |
| 類型 TEXT, | |
| 預計日期 DATE, | |
| 實際日期 DATE, | |
| 狀態 TEXT DEFAULT '待進行', | |
| 負責單位 TEXT, | |
| 備註 TEXT | |
| ) | |
| """) | |
| # 風險事件 (NEW) | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS risk_events ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 風險類型 TEXT, | |
| 描述 TEXT, | |
| 影響金額 REAL, | |
| 發生機率 TEXT, | |
| 影響程度 TEXT, | |
| 緩解措施 TEXT, | |
| 狀態 TEXT DEFAULT '監控中', | |
| 建立日期 DATE DEFAULT CURRENT_DATE | |
| ) | |
| """) | |
| # 情境模擬 (NEW) | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS scenarios ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 情境名稱 TEXT NOT NULL, | |
| 銷售延遲月數 INTEGER DEFAULT 0, | |
| 成本增幅比例 REAL DEFAULT 0, | |
| 利率調整 REAL DEFAULT 0, | |
| 市場價格調整 REAL DEFAULT 0, | |
| 備註 TEXT | |
| ) | |
| """) | |
| # 土地取得成本 (NEW - 含合作與收購) | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS land_costs ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 地主姓名 TEXT, | |
| 取得方式 TEXT, | |
| 土地面積坪 REAL, | |
| 權利價值萬 REAL, | |
| 收購價格萬 REAL DEFAULT 0, | |
| 付款狀態 TEXT DEFAULT '待付款', | |
| 備註 TEXT | |
| ) | |
| """) | |
| # 專案元數據 (NEW - 儲存日期等非數值設定) | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS project_metadata ( | |
| key TEXT PRIMARY KEY, | |
| value TEXT | |
| ) | |
| """) | |
| conn.commit() | |
| conn.close() | |
| def get_metadata(key: str, default: str = "") -> str: | |
| """Get a metadata value by key. | |
| Args: | |
| key: Metadata key. | |
| default: Default value if key not found. | |
| Returns: | |
| str: Metadata value. | |
| """ | |
| conn = get_connection() | |
| c = conn.cursor() | |
| c.execute("SELECT value FROM project_metadata WHERE key = ?", (key,)) | |
| row = c.fetchone() | |
| conn.close() | |
| return row["value"] if row else default | |
| def set_metadata(key: str, value: str) -> None: | |
| """Set a metadata value. | |
| Args: | |
| key: Metadata key. | |
| value: Metadata value. | |
| """ | |
| conn = get_connection() | |
| c = conn.cursor() | |
| c.execute("INSERT OR REPLACE INTO project_metadata VALUES (?, ?)", (key, str(value))) | |
| conn.commit() | |
| conn.close() | |
| def clear_to_actual() -> None: | |
| """Clear all data and reset to blank project state.""" | |
| conn = get_connection() | |
| c = conn.cursor() | |
| tables = [ | |
| "finance_transactions", | |
| "project_params", | |
| "project_stages", | |
| "loan_contracts", | |
| "inventory", | |
| "landowners", | |
| "milestones", | |
| "risk_events", | |
| "scenarios", | |
| "land_costs", | |
| "project_metadata", | |
| ] | |
| for tbl in tables: | |
| c.execute(f"DELETE FROM {tbl}") # noqa: S608 | |
| # 設定預設參數 | |
| c.execute("INSERT INTO project_params VALUES (?, ?)", ("地主分回比例", 0.60)) | |
| c.execute("INSERT INTO project_params VALUES (?, ?)", ("總工程預算", 100000)) | |
| conn.commit() | |
| conn.close() | |
| def load_full_project_example(total_budget: float) -> None: | |
| """Load comprehensive example data for demonstration. | |
| Args: | |
| total_budget: Total project budget in 萬 (10,000 TWD). | |
| """ | |
| conn = get_connection() | |
| c = conn.cursor() | |
| clear_to_actual() | |
| # 更新預算參數 | |
| c.execute( | |
| "INSERT OR REPLACE INTO project_params VALUES (?, ?)", | |
| ("總工程預算", total_budget), | |
| ) | |
| # ========== 1. 工程階段 (含設計成本) ========== | |
| design_cost = total_budget * 0.03 # 設計費約 3% | |
| stages = [ | |
| ("規劃設計", -6, 6, 3.0, design_cost), # 專案啟動前 6 個月開始 | |
| ("拆遷補償", 0, 4, 10.0, total_budget * 0.10), | |
| ("結構工程", 12, 18, 70.0, total_budget * 0.70), | |
| ("機電工程", 24, 8, 8.0, total_budget * 0.08), | |
| ("裝修交屋", 30, 6, 9.0, total_budget * 0.09), | |
| ] | |
| for s in stages: | |
| c.execute( | |
| """INSERT INTO project_stages | |
| (工項名稱, 開始月份, 持續月份, 成本佔比, 撥款佔比, 預算金額, 實際支出) | |
| VALUES (?,?,?,?,?,?,?)""", | |
| (s[0], s[1], s[2], s[3], s[3], s[4], s[4] * 0.95), | |
| ) | |
| # ========== 2. 融資合約 (含土地融資) ========== | |
| # 土地融資 - 專案啟動前就發生 | |
| land_loan = 5000.0 # 土地收購融資 5000 萬 | |
| c.execute( | |
| """INSERT INTO loan_contracts | |
| (貸款名稱, 授信額度, 年利率, 狀態, 動撥金額, 到期日, 備註) | |
| VALUES (?,?,?,?,?,?,?)""", | |
| ("土地融資", land_loan, 2.8, "已動撥", land_loan, "2027-12-31", "收購土地用"), | |
| ) | |
| # 土建融資 | |
| construction_loan = total_budget * 0.7 | |
| c.execute( | |
| """INSERT INTO loan_contracts | |
| (貸款名稱, 授信額度, 年利率, 狀態, 動撥金額, 到期日, 備註) | |
| VALUES (?,?,?,?,?,?,?)""", | |
| ("都更土建融", construction_loan, 2.3, "已動撥", construction_loan * 0.8, "2029-12-31", "建築工程融資"), | |
| ) | |
| # ========== 3. 銷售庫存 ========== | |
| c.execute( | |
| """INSERT INTO inventory | |
| (物件名稱, 預計售價, 總數量, 地主分回數量, 已售數量, 銷售狀態) | |
| VALUES (?,?,?,?,?,?)""", | |
| ("都更住宅", 3000.0, 100, 60, 15, "銷售中"), | |
| ) | |
| c.execute( | |
| """INSERT INTO inventory | |
| (物件名稱, 預計售價, 總數量, 地主分回數量, 已售數量, 銷售狀態) | |
| VALUES (?,?,?,?,?,?)""", | |
| ("車位", 350.0, 80, 48, 10, "銷售中"), | |
| ) | |
| # ========== 4. 地主資料 (含釘子戶) ========== | |
| landowners_data = [ | |
| ("王大明", 15.5, 45.0, "3房", "已簽署", "0912-345-678"), | |
| ("李美玲", 12.0, 36.0, "2房", "已簽署", "0923-456-789"), | |
| ("張志強", 8.5, 25.5, "2房", "協商中", "0934-567-890"), | |
| ("陳淑芬", 10.0, 30.0, "2房", "已簽署", "0945-678-901"), | |
| ("林建宏", 6.0, 18.0, "1房", "已簽署", "0956-789-012"), | |
| ("黃雅婷", 8.0, 24.0, "2房", "已簽署", "0967-890-123"), | |
| # 釘子戶 - 持分小但開價高 | |
| ("周先生", 1.0, 3.0, "套房", "拒絕中", "0978-901-234"), | |
| ] | |
| for ld in landowners_data: | |
| c.execute( | |
| """INSERT INTO landowners | |
| (姓名, 持分比例, 分回坪數, 分回戶型, 同意書狀態, 聯絡電話) | |
| VALUES (?,?,?,?,?,?)""", | |
| ld, | |
| ) | |
| # ========== 5. 土地取得成本 (含收購與合作) ========== | |
| # 土地合作 (以土地參與) | |
| land_costs_data = [ | |
| ("王大明", "土地合作", 50.0, 4500.0, 0, "已完成"), | |
| ("李美玲", "土地合作", 38.0, 3420.0, 0, "已完成"), | |
| ("張志強", "土地合作", 27.0, 2430.0, 0, "協商中"), | |
| # 收購案例 | |
| ("陳淑芬", "收購", 32.0, 2880.0, 2880.0, "已付清"), | |
| ("林建宏", "收購", 19.0, 1710.0, 1710.0, "已付清"), | |
| # 釘子戶 - 市價 350萬/坪 × 3坪 = 1050萬,開價 2000萬 | |
| ("周先生", "收購", 3.0, 1050.0, 0, "談判中"), # 尚未成交 | |
| ] | |
| for lc in land_costs_data: | |
| c.execute( | |
| """INSERT INTO land_costs | |
| (地主姓名, 取得方式, 土地面積坪, 權利價值萬, 收購價格萬, 付款狀態) | |
| VALUES (?,?,?,?,?,?)""", | |
| lc, | |
| ) | |
| # ========== 6. 里程碑 ========== | |
| base_date = datetime.date(2026, 1, 20) | |
| milestones_data = [ | |
| ("都更計畫核定", "政府審核", 30, "已完成"), | |
| ("事業計畫核定", "政府審核", 180, "進行中"), | |
| ("建照取得", "政府審核", 365, "待進行"), | |
| ("拆遷作業開始", "工程", 400, "待進行"), | |
| ("結構體完成", "工程", 800, "待進行"), | |
| ("使用執照取得", "政府審核", 1000, "待進行"), | |
| ("交屋作業", "銷售", 1100, "待進行"), | |
| ] | |
| for md in milestones_data: | |
| target_date = base_date + datetime.timedelta(days=md[2]) | |
| actual_date = target_date if md[3] == "已完成" else None | |
| c.execute( | |
| """INSERT INTO milestones | |
| (里程碑名稱, 類型, 預計日期, 實際日期, 狀態) | |
| VALUES (?,?,?,?,?)""", | |
| (md[0], md[1], target_date.isoformat(), actual_date, md[3]), | |
| ) | |
| # ========== 7. 風險事件 (含釘子戶風險) ========== | |
| risks_data = [ | |
| ("成本風險", "營造原物料價格上漲", total_budget * 0.05, "中", "高", "合約價格鎖定條款"), | |
| ("時程風險", "政府審核延遲", total_budget * 0.02, "中", "中", "提前送件、定期追蹤"), | |
| ("市場風險", "房市價格下跌", total_budget * 0.10, "低", "高", "分散銷售時程"), | |
| ("地主風險", "周先生收購談判 (開價2000萬 vs 市價1050萬)", 950.0, "高", "高", "評估替代方案或提高補償"), | |
| ("融資風險", "利率上升影響", total_budget * 0.03, "中", "中", "固定利率或利率避險"), | |
| ] | |
| for rd in risks_data: | |
| c.execute( | |
| """INSERT INTO risk_events | |
| (風險類型, 描述, 影響金額, 發生機率, 影響程度, 緩解措施) | |
| VALUES (?,?,?,?,?,?)""", | |
| rd, | |
| ) | |
| # ========== 8. 預設情境 ========== | |
| scenarios_data = [ | |
| ("基準情境", 0, 0, 0, 0), | |
| ("銷售延遲 6 個月", 6, 0, 0, 0), | |
| ("成本上漲 10%", 0, 10, 0, 0), | |
| ("利率上升 1%", 0, 0, 1.0, 0), | |
| ("悲觀情境", 6, 10, 1.0, -5), | |
| ("釘子戶接受開價", 0, 0, 0, 0), # 需額外支出 950萬 | |
| ] | |
| for sd in scenarios_data: | |
| c.execute( | |
| """INSERT INTO scenarios | |
| (情境名稱, 銷售延遲月數, 成本增幅比例, 利率調整, 市場價格調整) | |
| VALUES (?,?,?,?,?)""", | |
| sd, | |
| ) | |
| # ========== 9. 現金流模擬 (含土地融資利息和設計費) ========== | |
| salable_val = (3000.0 * 100) * 0.4 | |
| # 專案前期支出 (月份 -6 到 0) | |
| for m in range(-6, 1): | |
| # 設計費用 | |
| if m >= -6 and m < 0: | |
| c.execute( | |
| "INSERT INTO finance_transactions VALUES (?, '實際', '規劃設計費', ?, '支出')", | |
| (m if m != 0 else 1, design_cost / 6), | |
| ) | |
| # 土地融資利息 (專案前就開始) | |
| if m % 3 == 0: | |
| land_interest = land_loan * 0.028 / 4 | |
| c.execute( | |
| "INSERT INTO finance_transactions VALUES (?, '實際', '土地融資利息', ?, '支出')", | |
| (m if m != 0 else 1, land_interest), | |
| ) | |
| # 36 個月現金流 | |
| for m in range(1, 37): | |
| # 工程支出 | |
| cost_value = float( | |
| sum( | |
| (total_budget * s[3] / 100) / s[2] | |
| for s in stages | |
| if s[1] <= m - 1 < s[1] + s[2] and s[1] >= 0 | |
| ) | |
| ) | |
| if cost_value > 0: | |
| c.execute( | |
| "INSERT INTO finance_transactions VALUES (?, '實際', '工程支出', ?, '支出')", | |
| (m, cost_value * 1.05), | |
| ) | |
| # 每季土建融資利息 | |
| if m % 3 == 0: | |
| c.execute( | |
| "INSERT INTO finance_transactions VALUES (?, '實際', '土建融資利息', ?, '支出')", | |
| (m, construction_loan * 0.023 / 4), | |
| ) | |
| # 第 24 個月開始預售收入 | |
| if m >= 24 and m % 4 == 0: | |
| presale_income = salable_val * 0.15 | |
| c.execute( | |
| "INSERT INTO finance_transactions VALUES (?, '實際', '預售訂金', ?, '收入')", | |
| (m, presale_income), | |
| ) | |
| # 最後一個月結算 | |
| if m == 36: | |
| c.execute( | |
| "INSERT INTO finance_transactions VALUES (?, '實際', '交屋尾款', ?, '收入')", | |
| (m, salable_val * 0.55), | |
| ) | |
| c.execute( | |
| "INSERT INTO finance_transactions VALUES (?, '實際', '代銷與稅費', ?, '支出')", | |
| (m, salable_val * 0.06), | |
| ) | |
| conn.commit() | |
| conn.close() | |
| def backup_current_project() -> str: | |
| """Backup current database to timestamped file. | |
| Returns: | |
| str: Path to the backup file. | |
| """ | |
| if os.path.exists(DB_PATH): | |
| ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") | |
| backup_path = f"backup_{ts}.db" | |
| shutil.copy2(DB_PATH, backup_path) | |
| return backup_path | |
| return "" | |
| def get_database_file() -> Optional[bytes]: | |
| """Read current database file as bytes. | |
| Returns: | |
| bytes: Database file content or None if file missing. | |
| """ | |
| if os.path.exists(DB_PATH): | |
| with open(DB_PATH, "rb") as f: | |
| return f.read() | |
| return None | |
| def restore_database_from_bytes(file_bytes: bytes) -> bool: | |
| """Restore database from bytes. | |
| Args: | |
| file_bytes: Database file content to restore. | |
| Returns: | |
| bool: True if successful. | |
| """ | |
| try: | |
| # Create a backup before overwriting | |
| backup_current_project() | |
| with open(DB_PATH, "wb") as f: | |
| f.write(file_bytes) | |
| return True | |
| except Exception as e: | |
| print(f"Restore failed: {e}") | |
| return False | |
| # ============ 查詢輔助函數 ============ | |
| def get_landowner_stats() -> dict[str, Any]: | |
| """Get landowner agreement statistics. | |
| Returns: | |
| dict: Statistics including total count, agreement rate, and status breakdown. | |
| """ | |
| conn = get_connection() | |
| df = pd.read_sql_query("SELECT * FROM landowners", conn) | |
| conn.close() | |
| if df.empty: | |
| return {"total": 0, "agreement_rate": 0, "status_counts": {}} | |
| status_counts = df["同意書狀態"].value_counts().to_dict() | |
| agreed = status_counts.get("已簽署", 0) | |
| total = len(df) | |
| return { | |
| "total": total, | |
| "agreement_rate": (agreed / total * 100) if total > 0 else 0, | |
| "status_counts": status_counts, | |
| "total_share": df["持分比例"].sum(), | |
| "agreed_share": df[df["同意書狀態"] == "已簽署"]["持分比例"].sum(), | |
| } | |
| def get_milestone_summary() -> dict[str, Any]: | |
| """Get milestone progress summary. | |
| Returns: | |
| dict: Summary including completed, in-progress, and upcoming milestones. | |
| """ | |
| conn = get_connection() | |
| df = pd.read_sql_query("SELECT * FROM milestones ORDER BY 預計日期", conn) | |
| conn.close() | |
| if df.empty: | |
| return {"total": 0, "completed": 0, "in_progress": 0, "upcoming": 0} | |
| status_counts = df["狀態"].value_counts().to_dict() | |
| return { | |
| "total": len(df), | |
| "completed": status_counts.get("已完成", 0), | |
| "in_progress": status_counts.get("進行中", 0), | |
| "upcoming": status_counts.get("待進行", 0), | |
| } | |
| def calculate_scenario_impact( | |
| scenario_id: int, base_profit: float, base_loan: float | |
| ) -> dict[str, float]: | |
| """Calculate financial impact of a scenario. | |
| Args: | |
| scenario_id: ID of the scenario to calculate. | |
| base_profit: Base profit amount. | |
| base_loan: Base loan amount. | |
| Returns: | |
| dict: Impact calculations including adjusted profit and additional costs. | |
| """ | |
| conn = get_connection() | |
| scenario = pd.read_sql_query( | |
| f"SELECT * FROM scenarios WHERE id = {scenario_id}", conn # noqa: S608 | |
| ) | |
| conn.close() | |
| if scenario.empty: | |
| return {"adjusted_profit": base_profit, "additional_cost": 0} | |
| s = scenario.iloc[0] | |
| # 計算影響 | |
| delay_cost = s["銷售延遲月數"] * (base_loan * 0.023 / 12) # 延遲的利息成本 | |
| cost_increase = base_profit * (s["成本增幅比例"] / 100) | |
| rate_impact = base_loan * (s["利率調整"] / 100) | |
| market_impact = base_profit * (s["市場價格調整"] / 100) | |
| additional_cost = delay_cost + cost_increase + rate_impact | |
| adjusted_profit = base_profit - additional_cost + market_impact | |
| return { | |
| "adjusted_profit": adjusted_profit, | |
| "additional_cost": additional_cost, | |
| "delay_cost": delay_cost, | |
| "cost_increase": cost_increase, | |
| "rate_impact": rate_impact, | |
| "market_impact": market_impact, | |
| } | |
| def get_budget_vs_actual() -> pd.DataFrame: | |
| """Get budget vs actual comparison for project stages. | |
| Returns: | |
| pd.DataFrame: Comparison data with variance calculations. | |
| """ | |
| conn = get_connection() | |
| df = pd.read_sql_query( | |
| "SELECT 工項名稱, 預算金額, 實際支出 FROM project_stages", conn | |
| ) | |
| conn.close() | |
| if not df.empty: | |
| df["差異"] = df["實際支出"] - df["預算金額"] | |
| df["差異率"] = ((df["差異"] / df["預算金額"]) * 100).round(1) | |
| return df |