diff --git a/backend/migrations/columns.py b/backend/migrations/columns.py index 9895f14..8143022 100644 --- a/backend/migrations/columns.py +++ b/backend/migrations/columns.py @@ -92,6 +92,12 @@ def migrate_add_columns(): "ALTER TABLE project_finances ADD COLUMN project_manager VARCHAR(100) NOT NULL DEFAULT ''") _add_column_if_missing(conn, "project_finances", "task_data", "ALTER TABLE project_finances ADD COLUMN task_data TEXT") + _add_column_if_missing(conn, "project_finances", "contact_name", + "ALTER TABLE project_finances ADD COLUMN contact_name VARCHAR(100) NOT NULL DEFAULT ''") + _add_column_if_missing(conn, "project_finances", "contact_phone", + "ALTER TABLE project_finances ADD COLUMN contact_phone VARCHAR(50) NOT NULL DEFAULT ''") + _add_column_if_missing(conn, "project_finances", "other_info", + "ALTER TABLE project_finances ADD COLUMN other_info VARCHAR(500) NOT NULL DEFAULT ''") conn.commit() print("[migrate] 加列迁移完成") diff --git a/backend/routes.py b/backend/routes.py index a201488..26ce6bd 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -23,7 +23,7 @@ TABLES = { "products": ("product_versions", ["product_name", "version", "version_goal", "priority", "start_date", "plan_date", "dev_done_date", "test_date", "launch_date", "status", "notes", "tenant"]), "finance": ("finance_records", ["month", "project_name", "record_type", "category", "amount", "occurred_date", "notes", "tenant"]), "tasks": ("project_tasks", ["project_id", "phase", "milestone", "task", "owner", "due_date", "blockers", "notes", "status", "sort_order", "priority", "tenant"]), - "projectFinances": ("project_finances", ["project_id", "tenant", "business_type", "customer_name", "sign_amount", "sign_month", "status", "sales_person", "owner", "total_rev", "total_gross", "total_payment", "total_cost", "total_paid", "budget_data", "start_date", "end_date", "task_type", "task_count", "service_fee_standard", "project_manager", "task_data", "project_code"]), + "projectFinances": ("project_finances", ["project_id", "tenant", "business_type", "customer_name", "sign_amount", "sign_month", "status", "sales_person", "owner", "total_rev", "total_gross", "total_payment", "total_cost", "total_paid", "budget_data", "start_date", "end_date", "task_type", "task_count", "service_fee_standard", "project_manager", "task_data", "project_code", "contact_name", "contact_phone", "other_info"]), } # ---------- 鉴权装饰器 ---------- @@ -268,6 +268,10 @@ def bootstrap(): _q_start = ((_now_month - 1) // 3) * 3 + 1 _q_range = range(_q_start, _q_start + 3) _q_months = [f"2026-{m:02d}" for m in _q_range] + _prev_q_start = ((_now_month - 4) // 3) * 3 + 1 + _prev_q_range = range(max(_prev_q_start, 1), _prev_q_start + 3) + _prev_q_months = [f"2026-{m:02d}" for m in _prev_q_range] + _prev_month = _now_month - 1 if _now_month > 1 else None all_metrics.append({ "total_projects": len(t_signed_pfs), "total_proposals": len(t_ops), @@ -277,23 +281,33 @@ def bootstrap(): "signed_annual": sum(x["sign_amount"] or 0 for x in t_pfs if x["status"] == "已签约"), "signed_q2": sum(x["sign_amount"] or 0 for x in t_pfs if x["status"] == "已签约" and (x.get("sign_month") or "")[:7] in _q_months), "signed_month": sum(x["sign_amount"] or 0 for x in t_pfs if x["status"] == "已签约" and (x.get("sign_month") or "")[:7] == f"2026-{_now_month:02d}"), + "signed_prev_q": sum(x["sign_amount"] or 0 for x in t_pfs if x["status"] == "已签约" and (x.get("sign_month") or "")[:7] in _prev_q_months), + "signed_prev_month": sum(x["sign_amount"] or 0 for x in t_pfs if x["status"] == "已签约" and (x.get("sign_month") or "")[:7] == f"2026-{_prev_month:02d}") if _prev_month else 0, "revenue_annual": t_sum_budget("rev", range(1, 13)), "revenue_q2": t_sum_budget("rev", _q_range), "monthly_revenue": t_sum_budget("rev", [_now_month]), + "revenue_prev_q": t_sum_budget("rev", _prev_q_range), + "revenue_prev_month": t_sum_budget("rev", [_prev_month]) if _prev_month else 0, "gross_annual": t_sum_budget("gross", range(1, 13)), "gross_q2": t_sum_budget("gross", _q_range), "monthly_net_profit": t_sum_budget("gross", [_now_month]), + "gross_prev_q": t_sum_budget("gross", _prev_q_range), + "gross_prev_month": t_sum_budget("gross", [_prev_month]) if _prev_month else 0, "payment_annual": t_sum_budget("payment", range(1, 13)), "payment_q2": t_sum_budget("payment", _q_range), "payment_month": t_sum_budget("payment", [_now_month]), + "payment_prev_q": t_sum_budget("payment", _prev_q_range), + "payment_prev_month": t_sum_budget("payment", [_prev_month]) if _prev_month else 0, "cost_annual": t_sum_budget("cost", range(1, 13)), "cost_q2": t_sum_budget("cost", _q_range), "cost_month": t_sum_budget("cost", [_now_month]), + "cost_prev_q": t_sum_budget("cost", _prev_q_range), + "cost_prev_month": t_sum_budget("cost", [_prev_month]) if _prev_month else 0, }) all_monthly.append(monthly_finance(conn, t)) all_recent.extend(rows(conn, "SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 4", [t])) agg = {} - for key in ["total_projects","total_proposals","total_products","upcoming_products","signed_amount","signed_annual","signed_q2","signed_month","revenue_annual","revenue_q2","monthly_revenue","gross_annual","gross_q2","monthly_net_profit","payment_annual","payment_q2","payment_month","cost_annual","cost_q2","cost_month"]: + for key in ["total_projects","total_proposals","total_products","upcoming_products","signed_amount","signed_annual","signed_q2","signed_month","signed_prev_q","signed_prev_month","revenue_annual","revenue_q2","monthly_revenue","revenue_prev_q","revenue_prev_month","gross_annual","gross_q2","monthly_net_profit","gross_prev_q","gross_prev_month","payment_annual","payment_q2","payment_month","payment_prev_q","payment_prev_month","cost_annual","cost_q2","cost_month","cost_prev_q","cost_prev_month"]: agg[key] = sum(m.get(key, 0) for m in all_metrics) merged_monthly = [] for i in range(12): @@ -341,18 +355,29 @@ def bootstrap(): _now_month = date.today().month _q_start = ((_now_month - 1) // 3) * 3 + 1 _q_range = range(_q_start, _q_start + 3) + _prev_q_start = ((_now_month - 4) // 3) * 3 + 1 + _prev_q_range = range(max(_prev_q_start, 1), _prev_q_start + 3) + _prev_month = _now_month - 1 if _now_month > 1 else None rev_annual = sum_budget("rev", range(1, 13)) gross_annual = sum_budget("gross", range(1, 13)) rev_q2 = sum_budget("rev", _q_range) gross_q2 = sum_budget("gross", _q_range) rev_month = sum_budget("rev", [_now_month]) gross_month = sum_budget("gross", [_now_month]) + rev_prev_q = sum_budget("rev", _prev_q_range) + gross_prev_q = sum_budget("gross", _prev_q_range) + rev_prev_month = sum_budget("rev", [_prev_month]) if _prev_month else 0 + gross_prev_month = sum_budget("gross", [_prev_month]) if _prev_month else 0 payment_annual = sum_budget("payment", range(1, 13)) cost_annual = sum_budget("cost", range(1, 13)) payment_q2 = sum_budget("payment", _q_range) cost_q2 = sum_budget("cost", _q_range) payment_month = sum_budget("payment", [_now_month]) cost_month = sum_budget("cost", [_now_month]) + payment_prev_q = sum_budget("payment", _prev_q_range) + cost_prev_q = sum_budget("cost", _prev_q_range) + payment_prev_month = sum_budget("payment", [_prev_month]) if _prev_month else 0 + cost_prev_month = sum_budget("cost", [_prev_month]) if _prev_month else 0 def pf_status_sum(status): return sum(x["sign_amount"] or 0 for x in pfs if x["status"] == status) signed_amount = pf_status_sum("已签约") @@ -360,6 +385,9 @@ def bootstrap(): _q_months = [f"2026-{m:02d}" for m in _q_range] signed_q2 = sum(x["sign_amount"] or 0 for x in pfs if x["status"] == "已签约" and (x.get("sign_month") or "")[:7] in _q_months) signed_month = sum(x["sign_amount"] or 0 for x in pfs if x["status"] == "已签约" and (x.get("sign_month") or "")[:7] == f"2026-{_now_month:02d}") + _prev_q_months = [f"2026-{m:02d}" for m in _prev_q_range] + signed_prev_q = sum(x["sign_amount"] or 0 for x in pfs if x["status"] == "已签约" and (x.get("sign_month") or "")[:7] in _prev_q_months) + signed_prev_month = sum(x["sign_amount"] or 0 for x in pfs if x["status"] == "已签约" and (x.get("sign_month") or "")[:7] == f"2026-{_prev_month:02d}") if _prev_month else 0 pipeline_amount = sum(x["expected_contract_amount"] or 0 for x in operations if x["project_status"] not in ["已签约","已丢单","已归档","已完成"]) signed_not_executed = sum(x["expected_contract_amount"] or 0 for x in operations if x["project_type"] == "execution" and x["execution_progress"] < 100) summary = { @@ -380,17 +408,27 @@ def bootstrap(): "signed_annual": signed_annual, "signed_q2": signed_q2, "signed_month": signed_month, + "signed_prev_q": signed_prev_q, + "signed_prev_month": signed_prev_month, "pipeline_amount": pipeline_amount, "revenue_annual": rev_annual, "revenue_q2": rev_q2, + "revenue_prev_q": rev_prev_q, + "revenue_prev_month": rev_prev_month, "gross_annual": gross_annual, "gross_q2": gross_q2, + "gross_prev_q": gross_prev_q, + "gross_prev_month": gross_prev_month, "payment_annual": payment_annual, "payment_q2": payment_q2, "payment_month": payment_month, + "payment_prev_q": payment_prev_q, + "payment_prev_month": payment_prev_month, "cost_annual": cost_annual, "cost_q2": cost_q2, "cost_month": cost_month, + "cost_prev_q": cost_prev_q, + "cost_prev_month": cost_prev_month, "signed_not_executed": signed_not_executed, }, "recent": q("SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 8", tenant), @@ -477,6 +515,17 @@ def update_resource(resource, item_id): # ---------- followups ---------- +@bp.route("/api/followups//") +def list_followups(target_type, target_id): + conn = db() + try: + fups = rows(conn, + "SELECT * FROM follow_up_records WHERE target_type=? AND target_id=? ORDER BY followed_at DESC, id DESC", + (target_type, target_id)) + return jsonify(fups) + finally: + conn.close() + @bp.route("/api/followups//", methods=["POST"]) @login_required def add_followup(target_type, target_id): diff --git a/static/modules/finance.js b/static/modules/finance.js index 1fe0963..4881e3b 100644 --- a/static/modules/finance.js +++ b/static/modules/finance.js @@ -1,6 +1,7 @@ // finance.js — 经营管理(财务)模块 const moneyInt = (v) => `${Math.round(Number(v || 0)).toLocaleString("zh-CN")} 元`; +const moneyWan = (v) => `${(Number(v || 0) / 10000).toFixed(1)} 万`; function renderFinance() { const pfs = state.data.projectFinances || []; @@ -73,7 +74,7 @@ function renderFinance() { const budget = JSON.parse(pf.budget_data || "[]"); budget.forEach(b => { budgetMap[(b.month || "").replace("-", "_")] = b; }); } catch (e) {} - const isRevView = state.finView !== "cashflow" && state.finView !== "overview" && state.finView !== "monthly"; + const isRevView = state.finView !== "cashflow" && state.finView !== "overview" && state.finView !== "monthly" && state.finView !== "quarterly"; const mCols = months.map(m => { const b = budgetMap[m] || {}; if (isRevView) { @@ -99,53 +100,55 @@ function renderFinance() { })(); const sm = pf.sign_month || ""; const signMonthCell = `${sm || '—'}`; - return `${esc(pf.customer_name)}${esc(pf.business_type)}${pf.status === "已签约" ? badge("已签约") : pf.status === "流程中" ? badge("流程中","blue") : badge("待签约","amber")}${signMonthCell}${money(pf.sign_amount)}${mCols}${totalCol}`; + return `${esc(pf.customer_name)}${signMonthCell}${money(pf.sign_amount)}${mCols}${totalCol}`; }; + const finHeaderBase = `|筛选:状态:`; + const finAddBtn = ``; + document.querySelector("#finance").innerHTML = `
- ${[["已签项目","" + signed.length,"file-check-2"],["签约金额",moneyInt(sumSign),"coins"],["待签项目","" + pending.length,"file-question"],["待签金额",moneyInt(sumPending),"hourglass"]].map(([l,v,icon]) => `
${l}${v}
`).join("")} + ${[["已签项目","" + signed.length,"file-check-2"],["签约金额",moneyWan(sumSign),"coins"],["待签项目","" + pending.length,"file-question"],["待签金额",moneyWan(sumPending),"hourglass"]].map(([l,v,icon]) => `
${l}${v}
`).join("")}
${[["本月确收",moneyInt(thisMonthRev),"trending-up"],["本月毛利",moneyInt(thisMonthGross),"percent"],["本月回款",moneyInt(monthPayment),"wallet"],["本月应付",moneyInt(monthCost),"receipt"],["本月现金流",moneyInt(monthCashflow),"repeat"]].map(([l,v,icon]) => `
${l}${v}
`).join("")}
-