Spaces:
Runtime error
Runtime error
Update src/app.py
Browse files- src/app.py +30 -48
src/app.py
CHANGED
|
@@ -7,56 +7,42 @@ import sqlite3
|
|
| 7 |
import os
|
| 8 |
from database import init_db, load_full_project_example, backup_current_project, restore_database, list_backups, DB_PATH
|
| 9 |
|
| 10 |
-
# 頁面初始化
|
| 11 |
-
st.set_page_config(page_title="SPV 全方位財務監控", layout="wide")
|
| 12 |
init_db()
|
|
|
|
| 13 |
|
| 14 |
-
# Session State
|
| 15 |
if "load_state" not in st.session_state:
|
| 16 |
st.session_state.load_state = "idle"
|
| 17 |
|
| 18 |
-
# --- 側邊欄
|
| 19 |
with st.sidebar:
|
| 20 |
st.header("⚙️ 系統管理")
|
| 21 |
total_b = st.number_input("總工程預算 (萬)", value=50000, step=1000)
|
| 22 |
project_start = st.date_input("計畫啟動日期", datetime.date(2026, 1, 20))
|
| 23 |
|
| 24 |
-
st.divider()
|
| 25 |
-
st.subheader("🔄 範例載入與還原")
|
| 26 |
if st.session_state.load_state == "idle":
|
| 27 |
if st.button("💡 載入全生命週期範例", use_container_width=True):
|
| 28 |
st.session_state.load_state = "confirm"
|
| 29 |
st.rerun()
|
| 30 |
else:
|
| 31 |
-
st.warning("載入將覆蓋
|
| 32 |
if st.button("✅ 確認載入", type="primary", use_container_width=True):
|
| 33 |
backup_current_project()
|
| 34 |
load_full_project_example(total_b)
|
| 35 |
st.session_state.load_state = "idle"
|
| 36 |
-
st.success("數據載入成功!")
|
| 37 |
st.rerun()
|
| 38 |
if st.button("❌ 取消", use_container_width=True):
|
| 39 |
st.session_state.load_state = "idle"
|
| 40 |
st.rerun()
|
| 41 |
|
| 42 |
-
st.
|
| 43 |
-
uploaded_file = st.file_uploader("📤 上傳 .db 還原", type="db")
|
| 44 |
if uploaded_file and st.button("🚀 執行還原"):
|
| 45 |
restore_database(uploaded_file)
|
| 46 |
-
st.success("還原成功!")
|
| 47 |
st.rerun()
|
| 48 |
|
| 49 |
-
|
| 50 |
-
backups = list_backups()
|
| 51 |
-
if backups:
|
| 52 |
-
sel = st.selectbox("選擇檔案", backups)
|
| 53 |
-
with open(sel, "rb") as f:
|
| 54 |
-
st.download_button("下載備份", f, file_name=sel)
|
| 55 |
-
|
| 56 |
-
# --- 主介面 Tabs ---
|
| 57 |
tabs = st.tabs(["📊 損益與地主試算", "💰 現金流總表", "🏗️ 工程管理", "🏦 融資管理", "🏠 房屋銷售"])
|
| 58 |
|
| 59 |
-
# --- Tab 1: 損益
|
| 60 |
with tabs[0]:
|
| 61 |
conn = sqlite3.connect(DB_PATH)
|
| 62 |
inv = pd.read_sql_query("SELECT * FROM inventory", conn)
|
|
@@ -64,58 +50,54 @@ with tabs[0]:
|
|
| 64 |
df_fin = pd.read_sql_query("SELECT * FROM finance_data WHERE 類型='實際'", conn)
|
| 65 |
conn.close()
|
| 66 |
|
| 67 |
-
if not inv.empty
|
| 68 |
ratio = params[params['參數名稱']=='地主分回比例']['數值'].values[0] if not params.empty else 0.6
|
| 69 |
total_val = (inv['預計售價'] * inv['總數量']).sum()
|
| 70 |
landowner_val = total_val * ratio
|
| 71 |
dev_val = total_val - landowner_val
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
costs = df_fin[['工程款', '管銷費用', '利息支出', '代銷費用', '其他稅費']].sum()
|
| 75 |
-
total_cost = costs.sum()
|
| 76 |
-
net_profit = dev_val - total_cost
|
| 77 |
-
|
| 78 |
-
st.subheader("⚖️ 地主分回與實施者利潤分析")
|
| 79 |
-
c1, c2, c3 = st.columns(3)
|
| 80 |
-
c1.metric("全案總銷估值", f"{total_val:,.0f} 萬")
|
| 81 |
-
c2.metric("地主分回價值", f"{landowner_val:,.0f} 萬", f"{ratio*100:.0f}%")
|
| 82 |
-
c3.metric("實施者可售價值", f"{dev_val:,.0f} 萬")
|
| 83 |
|
| 84 |
-
|
| 85 |
fig_w = go.Figure(go.Waterfall(
|
| 86 |
-
orientation = "v",
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
y = [dev_val, -costs['工程款'], -costs['利息支出'], -(costs['代銷費用']+costs['其他稅費']), 0],
|
| 90 |
connector = {"line":{"color":"rgb(63, 63, 63)"}},
|
| 91 |
))
|
| 92 |
st.plotly_chart(fig_w, use_container_width=True)
|
| 93 |
-
else:
|
| 94 |
-
st.info("請先載入範例數據以啟動損益分析。")
|
| 95 |
|
| 96 |
-
# --- Tab 2: 現金流總表 (色系
|
| 97 |
with tabs[1]:
|
| 98 |
conn = sqlite3.connect(DB_PATH)
|
| 99 |
df_act = pd.read_sql_query("SELECT * FROM finance_data WHERE 類型='實際' ORDER BY 月份 ASC", conn)
|
| 100 |
conn.close()
|
| 101 |
if not df_act.empty:
|
| 102 |
-
num_cols = ['工程款', '貸款撥入', '利息支出', '貸款還本', '預售收入', '代銷費用'
|
| 103 |
st.subheader("📅 現金流明細 (🟢 流入 / 🔴 流出)")
|
| 104 |
st.dataframe(df_act.style.format(subset=num_cols, formatter="{:,.0f}")
|
| 105 |
.applymap(lambda x: 'color: #228B22; font-weight: bold', subset=['貸款撥入', '預售收入'])
|
| 106 |
.applymap(lambda x: 'color: #DC143C', subset=['工程款', '利息支出', '貸款還本', '代銷費用']),
|
| 107 |
use_container_width=True)
|
| 108 |
-
else:
|
| 109 |
-
st.info("尚無執行數據。")
|
| 110 |
|
| 111 |
-
# --- Tab 3:
|
| 112 |
with tabs[2]:
|
| 113 |
conn = sqlite3.connect(DB_PATH)
|
| 114 |
stages = pd.read_sql_query("SELECT * FROM project_stages", conn)
|
| 115 |
-
conn.close()
|
| 116 |
if not stages.empty:
|
| 117 |
-
st.subheader("🏗️ 工程進度排程")
|
| 118 |
df_g = [dict(Task=r['工項名稱'], Start=(project_start + datetime.timedelta(days=r['開始月份']*30.4)),
|
| 119 |
Finish=(project_start + datetime.timedelta(days=(r['開始月份']+r['持續月份'])*30.4)), Resource='工程') for _, r in stages.iterrows()]
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
import os
|
| 8 |
from database import init_db, load_full_project_example, backup_current_project, restore_database, list_backups, DB_PATH
|
| 9 |
|
|
|
|
|
|
|
| 10 |
init_db()
|
| 11 |
+
st.set_page_config(page_title="SPV 全方位財務監控", layout="wide")
|
| 12 |
|
|
|
|
| 13 |
if "load_state" not in st.session_state:
|
| 14 |
st.session_state.load_state = "idle"
|
| 15 |
|
| 16 |
+
# --- 側邊欄 ---
|
| 17 |
with st.sidebar:
|
| 18 |
st.header("⚙️ 系統管理")
|
| 19 |
total_b = st.number_input("總工程預算 (萬)", value=50000, step=1000)
|
| 20 |
project_start = st.date_input("計畫啟動日期", datetime.date(2026, 1, 20))
|
| 21 |
|
|
|
|
|
|
|
| 22 |
if st.session_state.load_state == "idle":
|
| 23 |
if st.button("💡 載入全生命週期範例", use_container_width=True):
|
| 24 |
st.session_state.load_state = "confirm"
|
| 25 |
st.rerun()
|
| 26 |
else:
|
| 27 |
+
st.warning("⚠️ 載入將覆蓋數據並備份。")
|
| 28 |
if st.button("✅ 確認載入", type="primary", use_container_width=True):
|
| 29 |
backup_current_project()
|
| 30 |
load_full_project_example(total_b)
|
| 31 |
st.session_state.load_state = "idle"
|
|
|
|
| 32 |
st.rerun()
|
| 33 |
if st.button("❌ 取消", use_container_width=True):
|
| 34 |
st.session_state.load_state = "idle"
|
| 35 |
st.rerun()
|
| 36 |
|
| 37 |
+
uploaded_file = st.file_uploader("📤 上傳還原 (.db)", type="db")
|
|
|
|
| 38 |
if uploaded_file and st.button("🚀 執行還原"):
|
| 39 |
restore_database(uploaded_file)
|
|
|
|
| 40 |
st.rerun()
|
| 41 |
|
| 42 |
+
# --- 主介面 ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
tabs = st.tabs(["📊 損益與地主試算", "💰 現金流總表", "🏗️ 工程管理", "🏦 融資管理", "🏠 房屋銷售"])
|
| 44 |
|
| 45 |
+
# --- Tab 1: 損益與地主分回瀑布圖 ---
|
| 46 |
with tabs[0]:
|
| 47 |
conn = sqlite3.connect(DB_PATH)
|
| 48 |
inv = pd.read_sql_query("SELECT * FROM inventory", conn)
|
|
|
|
| 50 |
df_fin = pd.read_sql_query("SELECT * FROM finance_data WHERE 類型='實際'", conn)
|
| 51 |
conn.close()
|
| 52 |
|
| 53 |
+
if not inv.empty:
|
| 54 |
ratio = params[params['參數名稱']=='地主分回比例']['數值'].values[0] if not params.empty else 0.6
|
| 55 |
total_val = (inv['預計售價'] * inv['總數量']).sum()
|
| 56 |
landowner_val = total_val * ratio
|
| 57 |
dev_val = total_val - landowner_val
|
| 58 |
+
costs = df_fin[['工程款', '利息支出', '代銷費用', '管銷費用']].sum()
|
| 59 |
+
net_profit = dev_val - costs.sum()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
+
st.subheader("⚖️ 實施者利潤瀑布分析")
|
| 62 |
fig_w = go.Figure(go.Waterfall(
|
| 63 |
+
orientation = "v", measure = ["absolute", "relative", "relative", "relative", "total"],
|
| 64 |
+
x = ["可售總額", "工程成本", "利息支出", "代銷與管銷", "預計淨利"],
|
| 65 |
+
y = [dev_val, -costs['工程款'], -costs['利息支出'], -(costs['代銷費用']+costs['管銷費用']), 0],
|
|
|
|
| 66 |
connector = {"line":{"color":"rgb(63, 63, 63)"}},
|
| 67 |
))
|
| 68 |
st.plotly_chart(fig_w, use_container_width=True)
|
|
|
|
|
|
|
| 69 |
|
| 70 |
+
# --- Tab 2: 現金流總表 (色系區分) ---
|
| 71 |
with tabs[1]:
|
| 72 |
conn = sqlite3.connect(DB_PATH)
|
| 73 |
df_act = pd.read_sql_query("SELECT * FROM finance_data WHERE 類型='實際' ORDER BY 月份 ASC", conn)
|
| 74 |
conn.close()
|
| 75 |
if not df_act.empty:
|
| 76 |
+
num_cols = ['工程款', '貸款撥入', '利息支出', '貸款還本', '預售收入', '代銷費用']
|
| 77 |
st.subheader("📅 現金流明細 (🟢 流入 / 🔴 流出)")
|
| 78 |
st.dataframe(df_act.style.format(subset=num_cols, formatter="{:,.0f}")
|
| 79 |
.applymap(lambda x: 'color: #228B22; font-weight: bold', subset=['貸款撥入', '預售收入'])
|
| 80 |
.applymap(lambda x: 'color: #DC143C', subset=['工程款', '利息支出', '貸款還本', '代銷費用']),
|
| 81 |
use_container_width=True)
|
|
|
|
|
|
|
| 82 |
|
| 83 |
+
# --- Tab 3, 4, 5: 修復空白分頁 ---
|
| 84 |
with tabs[2]:
|
| 85 |
conn = sqlite3.connect(DB_PATH)
|
| 86 |
stages = pd.read_sql_query("SELECT * FROM project_stages", conn)
|
|
|
|
| 87 |
if not stages.empty:
|
|
|
|
| 88 |
df_g = [dict(Task=r['工項名稱'], Start=(project_start + datetime.timedelta(days=r['開始月份']*30.4)),
|
| 89 |
Finish=(project_start + datetime.timedelta(days=(r['開始月份']+r['持續月份'])*30.4)), Resource='工程') for _, r in stages.iterrows()]
|
| 90 |
+
st.plotly_chart(ff.create_gantt(df_g, index_col='Resource', group_tasks=True), use_container_width=True)
|
| 91 |
+
conn.close()
|
| 92 |
+
|
| 93 |
+
with tabs[3]:
|
| 94 |
+
st.subheader("🏦 融資合約清單")
|
| 95 |
+
conn = sqlite3.connect(DB_PATH)
|
| 96 |
+
st.dataframe(pd.read_sql_query("SELECT 貸款名稱, 授信額度, 年利率, 狀態 FROM loan_contracts", conn), use_container_width=True)
|
| 97 |
+
conn.close()
|
| 98 |
+
|
| 99 |
+
with tabs[4]:
|
| 100 |
+
st.subheader("🏠 房屋銷售清單")
|
| 101 |
+
conn = sqlite3.connect(DB_PATH)
|
| 102 |
+
st.dataframe(pd.read_sql_query("SELECT 物件名稱, 預計售價, 總數量, 地主分回數量, 銷售狀態 FROM inventory", conn), use_container_width=True)
|
| 103 |
+
conn.close()
|