diff --git a/backend/helpers.py b/backend/helpers.py index 3cd945f..a04335f 100644 --- a/backend/helpers.py +++ b/backend/helpers.py @@ -80,13 +80,14 @@ def monthly_finance(conn, tenant="科普·无界"): "gross": float(b.get("gross") or 0), "payment": float(b.get("payment") or 0), "cost": float(b.get("cost") or 0), + "paid": float(b.get("paid") or 0), } parsed_budgets.append((pf, budget_map)) data = [] for month in months: key = month.replace("-", "_") - revenue = gross = payment = cost = sign = 0 + revenue = gross = payment = cost = paid = sign = 0 for pf, budget_map in parsed_budgets: if pf["status"] == "已签约" and (pf.get("sign_month") or "") == month: sign += float(pf["sign_amount"] or 0) @@ -96,10 +97,14 @@ def monthly_finance(conn, tenant="科普·无界"): gross += b["gross"] payment += b["payment"] cost += b["cost"] + paid += b["paid"] data.append({ - "month": month, "revenue": revenue, - "labor": 0, "expense": 0, "purchase": 0, - "gross": gross, - "sign": sign, "payment": payment, "cost": cost, + "month": month, + "revenue": round(revenue, 2), + "gross": round(gross, 2), + "payment": round(payment, 2), + "cost": round(cost, 2), + "paid": round(paid, 2), + "sign": round(sign, 2), }) return data diff --git a/backend/routes.py b/backend/routes.py index 20e21e4..000bb27 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -322,15 +322,30 @@ def bootstrap(): all_metrics[-1]["expense_month"] = t_expense_sum([_now_month]) all_metrics[-1]["expense_prev_q"] = t_expense_sum(_prev_q_range) all_metrics[-1]["expense_prev_month"] = t_expense_sum([_prev_month]) if _prev_month else 0 + all_metrics[-1]["paid_annual"] = t_sum_budget("paid", range(1, 13)) + all_metrics[-1]["paid_q2"] = t_sum_budget("paid", _q_range) + all_metrics[-1]["paid_month"] = t_sum_budget("paid", [_now_month]) + all_metrics[-1]["paid_prev_q"] = t_sum_budget("paid", _prev_q_range) + all_metrics[-1]["paid_prev_month"] = t_sum_budget("paid", [_prev_month]) if _prev_month else 0 + all_metrics[-1]["cashflow_annual"] = all_metrics[-1]["payment_annual"] - all_metrics[-1]["paid_annual"] + all_metrics[-1]["cashflow_q2"] = all_metrics[-1]["payment_q2"] - all_metrics[-1]["paid_q2"] + all_metrics[-1]["cashflow_month"] = all_metrics[-1]["payment_month"] - all_metrics[-1]["paid_month"] + all_metrics[-1]["cashflow_prev_q"] = all_metrics[-1]["payment_prev_q"] - all_metrics[-1]["paid_prev_q"] + all_metrics[-1]["cashflow_prev_month"] = all_metrics[-1]["payment_prev_month"] - all_metrics[-1]["paid_prev_month"] + all_metrics[-1]["profit_annual"] = all_metrics[-1]["gross_annual"] - all_metrics[-1]["expense_annual"] + all_metrics[-1]["profit_q2"] = all_metrics[-1]["gross_q2"] - all_metrics[-1]["expense_q2"] + all_metrics[-1]["profit_month"] = all_metrics[-1]["monthly_net_profit"] - all_metrics[-1]["expense_month"] + all_metrics[-1]["profit_prev_q"] = all_metrics[-1]["gross_prev_q"] - all_metrics[-1]["expense_prev_q"] + all_metrics[-1]["profit_prev_month"] = all_metrics[-1]["gross_prev_month"] - all_metrics[-1]["expense_prev_month"] 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","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","expense_annual","expense_q2","expense_month","expense_prev_q","expense_prev_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","expense_annual","expense_q2","expense_month","expense_prev_q","expense_prev_month","paid_annual","paid_q2","paid_month","paid_prev_q","paid_prev_month","cashflow_annual","cashflow_q2","cashflow_month","cashflow_prev_q","cashflow_prev_month","profit_annual","profit_q2","profit_month","profit_prev_q","profit_prev_month"]: agg[key] = sum(m.get(key, 0) for m in all_metrics) merged_monthly = [] for i in range(12): m = {"month": all_monthly[0][i]["month"] if all_monthly and len(all_monthly[0]) > i else f"2026-{i+1:02d}"} - for field in ["revenue","gross","payment","cost","sign"]: + for field in ["revenue","gross","payment","cost","paid","sign"]: m[field] = sum(tl[i][field] if i < len(tl) else 0 for tl in all_monthly) merged_monthly.append(m) summary = { @@ -412,6 +427,21 @@ def bootstrap(): expense_month_val = expense_sum([_now_month]) expense_prev_q = expense_sum(_prev_q_range) expense_prev_month = expense_sum([_prev_month]) if _prev_month else 0 + paid_annual = sum_budget("paid", range(1, 13)) + paid_q2 = sum_budget("paid", _q_range) + paid_month = sum_budget("paid", [_now_month]) + paid_prev_q = sum_budget("paid", _prev_q_range) + paid_prev_month = sum_budget("paid", [_prev_month]) if _prev_month else 0 + cashflow_annual = payment_annual - paid_annual + cashflow_q2 = payment_q2 - paid_q2 + cashflow_month = payment_month - paid_month + cashflow_prev_q = payment_prev_q - paid_prev_q + cashflow_prev_month = payment_prev_month - paid_prev_month + profit_annual = gross_annual - expense_annual + profit_q2 = gross_q2 - expense_q2 + profit_month = gross_month - expense_month_val + profit_prev_q = gross_prev_q - expense_prev_q + profit_prev_month = gross_prev_month - expense_prev_month 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("已签约") @@ -468,6 +498,21 @@ def bootstrap(): "expense_month": expense_month_val, "expense_prev_q": expense_prev_q, "expense_prev_month": expense_prev_month, + "paid_annual": paid_annual, + "paid_q2": paid_q2, + "paid_month": paid_month, + "paid_prev_q": paid_prev_q, + "paid_prev_month": paid_prev_month, + "cashflow_annual": cashflow_annual, + "cashflow_q2": cashflow_q2, + "cashflow_month": cashflow_month, + "cashflow_prev_q": cashflow_prev_q, + "cashflow_prev_month": cashflow_prev_month, + "profit_annual": profit_annual, + "profit_q2": profit_q2, + "profit_month": profit_month, + "profit_prev_q": profit_prev_q, + "profit_prev_month": profit_prev_month, "signed_not_executed": signed_not_executed, }, "recent": q("SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 8", tenant), diff --git a/static/modules/finance.js b/static/modules/finance.js index 724cd70..f720675 100644 --- a/static/modules/finance.js +++ b/static/modules/finance.js @@ -186,12 +186,12 @@ function renderFinance() {

¥0

-

总费用

+

总项目费用

¥0

- +
月份确收毛利回款应付已付费用
月份确收毛利回款应付已付项目费用
@@ -235,14 +235,14 @@ function renderFinance() { { label: '执行率', value: ctx => ctx.sumRev && ctx.signTotal ? Math.round(ctx.sumRev / ctx.signTotal * 100) + '%' : '—', color: 'text-slate-500' }, { label: '毛利', value: ctx => money(ctx.sumGross), color: 'text-green-700' }, { label: '毛利率', value: ctx => ctx.sumRev && ctx.sumGross ? Math.round(ctx.sumGross / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' }, - { label: '费用', value: ctx => money(ctx.sumExpense), color: 'text-orange-700' }, + { label: '项目费用', value: ctx => money(ctx.sumExpense), color: 'text-orange-700' }, { label: '已回款', value: ctx => moneyWan(ctx.sumPay), color: 'text-amber-700' }, { label: '回款率', value: ctx => ctx.sumRev && ctx.sumPay ? Math.round(ctx.sumPay / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' }, { label: '应付', value: ctx => moneyWan(ctx.sumCost), color: 'text-rose-700' }, { label: '已付', value: ctx => moneyWan(ctx.sumPaid), color: 'text-purple-700' }, { label: '付款率', value: ctx => ctx.sumCost && ctx.sumPaid ? Math.round(ctx.sumPaid / ctx.sumCost * 100) + '%' : '—', color: 'text-slate-500' }, { label: '现金流', value: ctx => { const v = ctx.sumPay - ctx.sumPaid; return { val: money(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null }, - { label: '利润', value: ctx => { const v = ctx.sumGross - ctx.sumExpense; return { val: moneyWan(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null }, + { label: '项目利润', value: ctx => { const v = ctx.sumGross - ctx.sumExpense; return { val: moneyWan(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null }, ]; const renderView = (rows, sumRev, sumPay, sumCost, sumPaid, sumGross, sumExpense, signTotal, signCnt, emptyText) => { @@ -264,9 +264,9 @@ function renderFinance() { '应付' + '已付' + '付款率' + - '费用' + + '项目费用' + '现金流' + - '利润' + + '项目利润' + ''; const tbody = rows.length ? `${rows.join("")}` : `${emptyText}`; diff --git a/static/modules/home.js b/static/modules/home.js index 3b7c039..5f484ae 100644 --- a/static/modules/home.js +++ b/static/modules/home.js @@ -39,12 +39,26 @@ function renderHome() { ["上季度累计", moneyInt(m.cost_prev_q || 0)], ["上月累计", moneyInt(m.cost_prev_month || 0)], ]; - const rows6 = [ - ["年度累计", moneyInt(m.expense_annual || 0)], - ["季度累计", moneyInt(m.expense_q2 || 0)], - ["本月新增", moneyInt(m.expense_month || 0)], - ["上季度累计", moneyInt(m.expense_prev_q || 0)], - ["上月累计", moneyInt(m.expense_prev_month || 0)], + const rows7 = [ + ["年度累计", moneyInt(m.paid_annual || 0)], + ["季度累计", moneyInt(m.paid_q2 || 0)], + ["本月新增", moneyInt(m.paid_month || 0)], + ["上季度累计", moneyInt(m.paid_prev_q || 0)], + ["上月累计", moneyInt(m.paid_prev_month || 0)], + ]; + const rows8 = [ + ["年度累计", moneyInt(m.cashflow_annual || 0)], + ["季度累计", moneyInt(m.cashflow_q2 || 0)], + ["本月新增", moneyInt(m.cashflow_month || 0)], + ["上季度累计", moneyInt(m.cashflow_prev_q || 0)], + ["上月累计", moneyInt(m.cashflow_prev_month || 0)], + ]; + const rows9 = [ + ["年度累计", moneyInt(m.profit_annual || 0)], + ["季度累计", moneyInt(m.profit_q2 || 0)], + ["本月新增", moneyInt(m.profit_month || 0)], + ["上季度累计", moneyInt(m.profit_prev_q || 0)], + ["上月累计", moneyInt(m.profit_prev_month || 0)], ]; const qoq = (cur, prev) => { if (!prev) return ''; @@ -53,49 +67,73 @@ function renderHome() { const sign = pct >= 0 ? '+' : ''; return '' + sign + pct + '%'; }; - const COLORS = ['text-slate-700','text-blue-600','text-green-600','text-amber-600','text-rose-600','text-orange-600']; - const LABELS = ['合同金额','确收金额','确收毛利','回款金额','应付金额','费用']; - const ALL_ROWS = [rows1, rows2, rows3, rows4, rows5, rows6]; - // 季度环比:Q 值 vs prev_Q 值;月度环比:month 值 vs prev_month 值 - const Q_FIELDS = [m.signed_q2||0, m.revenue_q2, m.gross_q2, m.payment_q2||0, m.cost_q2||0, m.expense_q2||0]; - const Q_PREV = [m.signed_prev_q||0, m.revenue_prev_q||0, m.gross_prev_q||0, m.payment_prev_q||0, m.cost_prev_q||0, m.expense_prev_q||0]; - const M_FIELDS = [m.signed_month||0, m.monthly_revenue, m.monthly_net_profit, m.payment_month||0, m.cost_month||0, m.expense_month||0]; - const M_PREV = [m.signed_prev_month||0, m.revenue_prev_month||0, m.gross_prev_month||0, m.payment_prev_month||0, m.cost_prev_month||0, m.expense_prev_month||0]; - var thead = ''; - for (var ci = 0; ci < 6; ci++) { - thead += '' + LABELS[ci] + '环比'; - } - thead += ''; + const COLORS = ['text-slate-700','text-blue-600','text-green-600','text-amber-600','text-rose-600','text-purple-600','text-cyan-600','text-indigo-600']; + const LABELS = ['合同金额','确收金额','确收毛利','回款金额','应付金额','已付','现金流','项目利润']; + const Q_FIELDS = [m.signed_q2||0, m.revenue_q2, m.gross_q2, m.payment_q2||0, m.cost_q2||0, m.paid_q2||0, m.cashflow_q2||0, m.profit_q2||0]; + const Q_PREV = [m.signed_prev_q||0, m.revenue_prev_q||0, m.gross_prev_q||0, m.payment_prev_q||0, m.cost_prev_q||0, m.paid_prev_q||0, m.cashflow_prev_q||0, m.profit_prev_q||0]; + const M_FIELDS = [m.signed_month||0, m.monthly_revenue, m.monthly_net_profit, m.payment_month||0, m.cost_month||0, m.paid_month||0, m.cashflow_month||0, m.profit_month||0]; + const M_PREV = [m.signed_prev_month||0, m.revenue_prev_month||0, m.gross_prev_month||0, m.payment_prev_month||0, m.cost_prev_month||0, m.paid_prev_month||0, m.cashflow_prev_month||0, m.profit_prev_month||0]; + const ROWS = [rows1, rows2, rows3, rows4, rows5, rows7, rows8, rows9]; + var thead = '指标年度累计季度累计季环比本月新增月环比上季度累计上月累计'; var tbody = ''; - for (var ri = 0; ri < 5; ri++) { - var rowLabel = rows1[ri][0]; - var rowHtml = '' + rowLabel + ''; - for (var ci = 0; ci < 6; ci++) { - rowHtml += '' + ALL_ROWS[ci][ri][1] + ''; - var momHtml = ''; - if (ri === 1) momHtml = qoq(Q_FIELDS[ci], Q_PREV[ci]); // 季度累计行:季度环比 - else if (ri === 2) momHtml = qoq(M_FIELDS[ci], M_PREV[ci]); // 本月新增行:月度环比 - else momHtml = ''; - rowHtml += '' + momHtml + ''; - } - rowHtml += ''; - tbody += rowHtml; + for (var ri = 0; ri < 8; ri++) { + var vals = ROWS[ri]; + tbody += '' + LABELS[ri] + '' + + '' + vals[0][1] + '' + + '' + vals[1][1] + '' + + '' + qoq(Q_FIELDS[ri], Q_PREV[ri]) + '' + + '' + vals[2][1] + '' + + '' + qoq(M_FIELDS[ri], M_PREV[ri]) + '' + + '' + vals[3][1] + '' + + '' + vals[4][1] + ''; } document.querySelector("#home").innerHTML = `
${state.tenant === "总工作台" ? "" : `
${[ - ["经营管理", m.total_projects, "finance"], + ["项目管理", m.total_projects, "finance"], ["重点工作与台账", m.total_proposals, "projects"], ["业务方案", m.total_products, "proposals"], ["产品迭代", m.upcoming_products, "products"], ].map(([label, value, tab]) => ``).join("")}
`} - ${card(`

财务概览

${thead}${tbody}
`, "p-4")} -
+ ${card(`

部门经营情况

+ ${(() => { + var d = [ + ['部门利润', [m.profit_annual||0, m.profit_q2||0, m.profit_month||0, m.profit_prev_q||0, m.profit_prev_month||0], + [m.profit_q2||0,m.profit_prev_q||0], [m.profit_month||0,m.profit_prev_month||0]], + ['部门费用', [m.expense_annual||0, m.expense_q2||0, (m.expense_month||0), m.expense_prev_q||0, m.expense_prev_month||0], + [m.expense_q2||0,m.expense_prev_q||0], [(m.expense_month||0),m.expense_prev_month||0]], + ['部门净利', [ + (m.profit_annual||0)-(m.expense_annual||0), (m.profit_q2||0)-(m.expense_q2||0), (m.profit_month||0)-(m.expense_month||0), + (m.profit_prev_q||0)-(m.expense_prev_q||0), (m.profit_prev_month||0)-(m.expense_prev_month||0) + ], [ + (m.profit_q2||0)-(m.expense_q2||0), (m.profit_prev_q||0)-(m.expense_prev_q||0) + ], [ + (m.profit_month||0)-(m.expense_month||0), (m.profit_prev_month||0)-(m.expense_prev_month||0) + ]] + ]; + var h = ''; + for (var di = 0; di < 3; di++) { + var r = d[di]; + h += '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + } + return h; + })()} +
指标年度累计季度累计季环比本月新增月环比上季度累计上月累计
' + r[0] + '' + moneyInt(r[1][0]) + '' + moneyInt(r[1][1]) + '' + qoq(r[2][0], r[2][1]) + '' + moneyInt(r[1][2]) + '' + qoq(r[3][0], r[3][1]) + '' + moneyInt(r[1][3]) + '' + moneyInt(r[1][4]) + '
`, "p-4")} + ${card(`

业务(项目)财务概览

${thead}${tbody}
`, "p-4")} +
${card(`

月度签约趋势

2026
`, "p-4")} ${card(`

月度确收与毛利

2026
`, "p-4")} - ${card(`

月度回款与费用

2026
`, "p-4")} + ${card(`

月度回款与已付

2026
`, "p-4")} + ${card(`

月度项目利润

2026
`, "p-4")}
${card(`

近期动态

${summary.recent.map((r) => `
${r.content}
${r.followed_at}
`).join("")}
`, "p-5")}
@@ -149,15 +187,28 @@ function renderCharts(data) { }); } - // 图3:月度回款与费用 + // 图3:月度回款与已付 const c3 = document.querySelector("#chartCash"); if (c3 && window.Chart) { if (state.chart3) state.chart3.destroy(); state.chart3 = new Chart(c3, { - type: "bar", + type: "line", data: { labels, datasets: [ - { label: "回款", data: data.map((x) => x.payment || 0), backgroundColor: "#d97706", borderRadius: 4 }, - { label: "费用", data: data.map((x) => x.cost || 0), backgroundColor: "#ef4444", borderRadius: 4 }, + { label: "回款", data: data.map((x) => x.payment || 0), borderColor: "#d97706", backgroundColor: "rgba(217,119,6,0.06)", fill: true, tension: 0.3 }, + { label: "已付", data: data.map((x) => x.cost || 0), borderColor: "#7c3aed", backgroundColor: "rgba(124,58,237,0.06)", fill: true, tension: 0.3 }, + ]}, + options: baseOpts, + }); + } + + // 图4:月度项目利润 + const c4 = document.querySelector("#chartProfit"); + if (c4 && window.Chart) { + if (state.chart4) state.chart4.destroy(); + state.chart4 = new Chart(c4, { + type: "line", + data: { labels, datasets: [ + { label: "利润", data: data.map((x) => (x.gross || 0)), borderColor: "#6366f1", backgroundColor: "rgba(99,102,241,0.06)", fill: true, tension: 0.3 }, ]}, options: baseOpts, }); diff --git a/static/modules/utils.js b/static/modules/utils.js index 9aeed51..42c9d11 100644 --- a/static/modules/utils.js +++ b/static/modules/utils.js @@ -14,6 +14,7 @@ const state = { chart: null, chart2: null, chart3: null, + chart4: null, productPlatform: "all", uploadTasks: [], expenseFilter: "全部",