From fa6c9b1711762480bc84d5b5266de18219ab0ab3 Mon Sep 17 00:00:00 2001 From: mac Date: Tue, 16 Jun 2026 16:43:44 +0800 Subject: [PATCH] =?UTF-8?q?v3.0=20=E2=80=94=20=E8=B4=A2=E5=8A=A1=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E4=B8=BA=E9=A1=B9=E7=9B=AE=E8=B4=A2=E5=8A=A1=E8=A7=86?= =?UTF-8?q?=E5=9B=BE=EF=BC=9A=E6=B1=87=E6=80=BB=E5=8D=A1=E7=89=87+?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E6=98=8E=E7=BB=86+=E6=9C=88=E5=BA=A6?= =?UTF-8?q?=E7=A1=AE=E6=94=B6/=E6=AF=9B=E5=88=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/flask_app.py | 59 +++++++++++++++++++++---------------------- static/app.js | 60 +++++++++++++++++++++++++++++--------------- 2 files changed, 68 insertions(+), 51 deletions(-) diff --git a/backend/flask_app.py b/backend/flask_app.py index d54c061..8795808 100644 --- a/backend/flask_app.py +++ b/backend/flask_app.py @@ -328,17 +328,20 @@ def monthly_finance(conn, tenant="科普·无界"): months.append(m.strftime("%Y-%m")) data = [] for month in months: - def s(cat): - return one(conn, "SELECT COALESCE(SUM(amount),0) AS v FROM finance_records WHERE month=? AND category=? AND tenant=?", (month, cat, tenant))["v"] - revenue = s("确认收入") + s("签单") - labor = s("人力成本") - expense = s("费用") - purchase = s("外部采购") - net = revenue - labor - expense - purchase + col_month = month.replace("-", "_") + col_rev = f"rev_{col_month}" + col_gross = f"gross_{col_month}" + # Only project_finances has columns for 2026-06 through 2026-09 + if month in ["2026-06", "2026-07", "2026-08", "2026-09"]: + revenue = one(conn, f"SELECT COALESCE(SUM({col_rev}),0) AS v FROM project_finances WHERE tenant=?", (tenant,))["v"] + gross = one(conn, f"SELECT COALESCE(SUM({col_gross}),0) AS v FROM project_finances WHERE tenant=?", (tenant,))["v"] + else: + revenue = 0 + gross = 0 data.append({ "month": month, "revenue": revenue, - "labor": labor, "expense": expense, "purchase": purchase, - "net_profit": net, + "labor": 0, "expense": 0, "purchase": 0, + "net_profit": gross, }) return data @@ -361,24 +364,17 @@ def bootstrap(): products = attach_common(conn, "products", q("SELECT * FROM product_versions WHERE tenant=? ORDER BY id DESC", tenant)) finance = q("SELECT * FROM finance_records WHERE tenant=? ORDER BY month DESC, id DESC", tenant) tasks = q("SELECT * FROM project_tasks WHERE tenant=? ORDER BY phase, sort_order, id", tenant) + pfs = q("SELECT * FROM project_finances WHERE tenant=? ORDER BY id DESC", tenant) current_month = "2026-06" - # Finance aggregates - def sum_cat(months, cat): - return sum(x["amount"] for x in finance if x["month"] in months and x.get("category") == cat) - months_2026 = [f"2026-{m:02d}" for m in range(1,7)] - months_q2 = ["2026-04","2026-05","2026-06"] - rev_annual = sum_cat(months_2026, "确认收入") + sum_cat(months_2026, "签单") - labor_annual = sum_cat(months_2026, "人力成本") - expense_annual = sum_cat(months_2026, "费用") - purchase_annual = sum_cat(months_2026, "外部采购") - rev_q2 = sum_cat(months_q2, "确认收入") + sum_cat(months_q2, "签单") - labor_q2 = sum_cat(months_q2, "人力成本") - expense_q2 = sum_cat(months_q2, "费用") - purchase_q2 = sum_cat(months_q2, "外部采购") - rev_month = sum_cat([current_month], "确认收入") + sum_cat([current_month], "签单") - labor_month = sum_cat([current_month], "人力成本") - expense_month = sum_cat([current_month], "费用") - purchase_month = sum_cat([current_month], "外部采购") + # Finance aggregates — from project_finances (project-based) + def pf_sum(field): + return sum(x[field] or 0 for x in pfs) + rev_month = pf_sum("rev_2026_06") + gross_month = pf_sum("gross_2026_06") + rev_q2 = pf_sum("rev_2026_06") + gross_q2 = pf_sum("gross_2026_06") + rev_annual = rev_q2 + gross_annual = gross_q2 # Contract aggregates — time-based signed_amount = sum(x["expected_contract_amount"] or 0 for x in operations if x["project_status"] == "已签约") from datetime import date @@ -402,8 +398,8 @@ def bootstrap(): "execution_projects": len([x for x in operations if x["project_type"] == "execution"]), "risk_projects": len([x for x in operations if x["project_status"] == "有风险" or x["risks"]]), "monthly_revenue": rev_month, - "monthly_net_profit": rev_month - labor_month - expense_month - purchase_month, - "monthly_gross": rev_month - labor_month - expense_month - purchase_month, + "monthly_net_profit": gross_month, + "monthly_gross": gross_month, "upcoming_products": len([x for x in products if x["status"] in ["规划中", "设计中", "开发中", "测试中"]]), "total_projects": len(operations), "total_proposals": len(proposals), @@ -416,14 +412,14 @@ def bootstrap(): "pipeline_amount": pipeline_amount, "revenue_annual": rev_annual, "revenue_q2": rev_q2, - "gross_annual": rev_annual - labor_annual - expense_annual - purchase_annual, - "gross_q2": rev_q2 - labor_q2 - expense_q2 - purchase_q2, + "gross_annual": gross_annual, + "gross_q2": gross_q2, "signed_not_executed": signed_not_executed, }, "recent": q("SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 8", tenant), "risks": [{"title": "执行提醒", "content": x["next_action"]} for x in operations if x["next_action"]][:5], } - return jsonify({"summary": summary, "sales": sales, "proposals": proposals, "operations": operations, "products": products, "finance": finance, "financeMonthly": monthly_finance(conn, tenant), "tasks": tasks, "tenant": tenant, "tenants": ["科普·无界","科研·无界","医患·无界"]}) + return jsonify({"summary": summary, "sales": sales, "proposals": proposals, "operations": operations, "products": products, "finance": finance, "projectFinances": pfs, "financeMonthly": monthly_finance(conn, tenant), "tasks": tasks, "tenant": tenant, "tenants": ["科普·无界","科研·无界","医患·无界"]}) finally: conn.close() @@ -435,6 +431,7 @@ TABLES = { "products": ("product_versions", ["product_name", "version", "version_goal", "feature_list", "launch_date", "status", "platform", "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", "tenant"]), + "projectFinances": ("project_finances", ["project_id", "tenant", "business_type", "customer_name", "sign_amount", "sign_month", "status", "sales_person", "rev_2026_06", "rev_2026_07", "rev_2026_08", "rev_2026_09", "gross_2026_06", "gross_2026_07", "gross_2026_08", "gross_2026_09"]), } diff --git a/static/app.js b/static/app.js index aae6b33..3584e1f 100644 --- a/static/app.js +++ b/static/app.js @@ -232,14 +232,18 @@ window.submitTaskForm = async (event, projectId) => { }; window.createFinance = async (event) => { event.preventDefault(); - const data = Object.fromEntries(new FormData(event.currentTarget).entries()); - // Map form: month(date)→month(YYYY-MM), category→category, amount, tenant - data.month = data.month.substring(0, 7); - data.project_name = state.tenant; + const form = event.currentTarget; + const data = Object.fromEntries(new FormData(form).entries()); data.tenant = state.tenant; - data.record_type = "revenue"; // default, will be adjusted by category + data.sign_amount = parseFloat(data.sign_amount) || 0; + for (const m of ["2026-06","2026-07","2026-08","2026-09"]) { + const k = m.replace("-","_"); + data["rev_"+k] = parseFloat(data["rev_"+k]) || 0; + data["gross_"+k] = parseFloat(data["gross_"+k]) || 0; + } try { - await api("/api/finance", { method: "POST", body: JSON.stringify({ data }) }); + await api("/api/projectFinances", { method: "POST", body: JSON.stringify({ data }) }); + form.reset(); await load(); } catch (error) { alert("新增失败:" + error.message); @@ -421,16 +425,35 @@ function renderProducts() { } function renderFinance() { - const rows = state.data.finance.map((x) => [x.month, x.category, money(x.amount), text(x.notes)]); + const pfs = state.data.projectFinances || []; + const ops = state.data.operations || []; + const months = ["2026-06","2026-07","2026-08","2026-09"]; + const monthLabels = ["6月","7月","8月","9月"]; + + // Aggregates + const signed = pfs.filter(x => x.status === "已签单"); + const pending = pfs.filter(x => x.status !== "已签单"); + const sumSign = signed.reduce((s,x) => s + (x.sign_amount||0), 0); + const sumPending = pending.reduce((s,x) => s + (x.sign_amount||0), 0); + const monthRev = months.map(m => pfs.reduce((s,x) => s + (x["rev_"+m.replace("-","_")]||0), 0)); + const monthGross = months.map(m => pfs.reduce((s,x) => s + (x["gross_"+m.replace("-","_")]||0), 0)); + + const renderPfRow = (pf) => { + const mCols = months.map(m => { + const rev = pf["rev_"+m.replace("-","_")] || 0; + const gross = pf["gross_"+m.replace("-","_")] || 0; + return `${rev ? money(rev) : '—'}
${gross ? money(gross) : '—'}`; + }).join(""); + return `${pf.customer_name}${pf.business_type}${pf.status === "已签单" ? badge("已签") : badge(pf.status,"amber")}${money(pf.sign_amount)}${mCols}${pf.sales_person || ""}`; + }; + document.querySelector("#finance").innerHTML = `
- ${card(`

收入、毛利、成本/费用、净利月度曲线

`, "p-5")} - ${card(formHtml([ - { label: "日期", input: `` }, - { label: "类型", input: `` }, - { label: "金额/万", input: `` }, - { label: "费用说明", input: `` }, - ], { handler: "createFinance", text: "新增明细" }), "p-4")} - ${card(`

明细列表 (${state.data.finance.length})

${renderTable(["日期", "类型", "金额", "备注"], rows)}`, "p-4")} +
+ ${[["已签项目","" + signed.length],["签约金额",money(sumSign)],["待签项目","" + pending.length],["待签金额",money(sumPending)],["本月确收",money(monthRev[0])],["本月毛利",money(monthGross[0])]].map(([l,v]) => `

${l}

${v}

`).join("")} +
+ ${card(`

月度趋势

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

新增项目财务

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

项目明细 (${pfs.length})

${monthLabels.map(l => ``).join("")}${pfs.map(renderPfRow).join("")}
客户类型状态签约金额${l}
确收/毛利
销售
`, "p-4")}
`; if (window.lucide) window.lucide.createIcons(); } @@ -452,11 +475,8 @@ function renderFinanceChart() { data: { labels: financeMonthly.map((x) => x.month), datasets: [ - { label: "确认收入", data: financeMonthly.map((x) => x.revenue), borderColor: "#2563eb", tension: 0.3 }, - { label: "人力成本", data: financeMonthly.map((x) => x.labor), borderColor: "#ef4444", tension: 0.3 }, - { label: "费用", data: financeMonthly.map((x) => x.expense), borderColor: "#d97706", tension: 0.3 }, - { label: "外部采购", data: financeMonthly.map((x) => x.purchase), borderColor: "#8b5cf6", tension: 0.3 }, - { label: "月度净利", data: financeMonthly.map((x) => x.net_profit), borderColor: "#059669", tension: 0.3, borderDash: [5,3] }, + { label: "月度确收", data: financeMonthly.map((x) => x.revenue), borderColor: "#2563eb", tension: 0.3 }, + { label: "月度毛利", data: financeMonthly.map((x) => x.net_profit), borderColor: "#059669", tension: 0.3 }, ], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: "bottom", labels: { boxWidth: 12, font: { size: 11 } } } }, scales: { x: { ticks: { font: { size: 11 } }, grid: { display: false } }, y: { ticks: { font: { size: 11 }, callback: (v) => v + "万" } } } },