Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa6c9b1711 |
@@ -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"]),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 `<td class="p-2 text-right whitespace-nowrap"><span class="${rev ? 'text-blue-700 font-medium' : 'text-slate-300'}">${rev ? money(rev) : '—'}</span><br><span class="text-xs ${gross ? 'text-green-600' : 'text-slate-300'}">${gross ? money(gross) : '—'}</span></td>`;
|
||||
}).join("");
|
||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50"><td class="p-2 text-sm font-medium">${pf.customer_name}</td><td class="p-2 text-sm">${pf.business_type}</td><td class="p-2 text-sm">${pf.status === "已签单" ? badge("已签") : badge(pf.status,"amber")}</td><td class="p-2 text-right text-sm">${money(pf.sign_amount)}</td>${mCols}<td class="p-2 text-sm text-slate-500">${pf.sales_person || ""}</td></tr>`;
|
||||
};
|
||||
|
||||
document.querySelector("#finance").innerHTML = `<div class="grid gap-4">
|
||||
${card(`<div class="flex items-center justify-between cursor-pointer" onclick="toggleFinanceChart()"><h2 class="text-lg font-bold text-slate-700">收入、毛利、成本/费用、净利月度曲线</h2><i data-lucide="chevron-down" class="transition-transform rotate-90" id="financeChartIcon"></i></div><div id="financeChartWrap" class="hidden mt-4"><div style="position:relative;height:300px"><canvas id="financeChart2"></canvas></div></div>`, "p-5")}
|
||||
${card(formHtml([
|
||||
{ label: "日期", input: `<input name="month" type="date" required>` },
|
||||
{ label: "类型", input: `<select name="category"><option>签单</option><option>确认收入</option><option>人力成本</option><option>费用</option><option>外部采购</option></select>` },
|
||||
{ label: "金额/万", input: `<input name="amount" type="number" step="0.01" required>` },
|
||||
{ label: "费用说明", input: `<input name="notes" placeholder="摘要说明">` },
|
||||
], { handler: "createFinance", text: "新增明细" }), "p-4")}
|
||||
${card(`<h3 class="font-bold text-slate-700 mb-3">明细列表 <span class="text-slate-400 font-normal">(${state.data.finance.length})</span></h3>${renderTable(["日期", "类型", "金额", "备注"], rows)}`, "p-4")}
|
||||
<div class="grid grid-cols-6 gap-3">
|
||||
${[["已签项目","" + signed.length],["签约金额",money(sumSign)],["待签项目","" + pending.length],["待签金额",money(sumPending)],["本月确收",money(monthRev[0])],["本月毛利",money(monthGross[0])]].map(([l,v]) => `<div class="bg-white rounded-lg border border-slate-200 p-3 text-center"><p class="text-xs text-slate-500">${l}</p><p class="text-xl font-bold text-slate-800">${v}</p></div>`).join("")}
|
||||
</div>
|
||||
${card(`<div class="flex items-center justify-between cursor-pointer" onclick="toggleFinanceChart()"><h2 class="text-lg font-bold text-slate-700">月度趋势</h2><i data-lucide="chevron-down" class="transition-transform rotate-90" id="financeChartIcon"></i></div><div id="financeChartWrap" class="hidden mt-4"><div style="position:relative;height:300px"><canvas id="financeChart2"></canvas></div></div>`, "p-5")}
|
||||
${card(`<div class="flex items-center justify-between cursor-pointer" onclick="document.querySelector('#pf-form-wrap').classList.toggle('hidden')"><h3 class="font-bold text-slate-700">新增项目财务</h3><i data-lucide="plus-circle" class="text-blue-600"></i></div><div id="pf-form-wrap" class="hidden mt-3"><form onsubmit="createFinance(event)" class="grid gap-3"><div class="grid grid-cols-3 gap-3"><label class="text-xs text-slate-500">项目<select name="project_id" class="w-full rounded border px-2 py-1.5 text-sm">${ops.map(o => `<option value="${o.id}">${o.project_name}</option>`).join("")}</select></label><label class="text-xs text-slate-500">业务类型<select name="business_type" class="w-full rounded border px-2 py-1.5 text-sm">${["患者管理","创新支付","电商","MCN","科普视频","科普运营","医患运营","真实世界研究","其他"].map(t => `<option>${t}</option>`).join("")}</select></label><label class="text-xs text-slate-500">客户名称<input name="customer_name" class="w-full rounded border px-2 py-1.5 text-sm"></label><label class="text-xs text-slate-500">签约金额/万<input name="sign_amount" type="number" step="0.01" class="w-full rounded border px-2 py-1.5 text-sm"></label><label class="text-xs text-slate-500">签约月份<input name="sign_month" class="w-full rounded border px-2 py-1.5 text-sm"></label><label class="text-xs text-slate-500">状态<select name="status" class="w-full rounded border px-2 py-1.5 text-sm"><option>已签单</option><option>待签</option></select></label></div><div class="grid grid-cols-8 gap-2">${["06","07","08","09"].map(m => `<div class="border rounded p-2"><p class="text-xs text-slate-400 mb-1">${m}月</p><label class="text-xs text-slate-500">确收<br><input name="rev_2026_${m}" type="number" step="0.01" class="w-full rounded border px-1 py-1 text-sm" placeholder="万"></label><label class="text-xs text-slate-500 mt-1">毛利<br><input name="gross_2026_${m}" type="number" step="0.01" class="w-full rounded border px-1 py-1 text-sm" placeholder="万"></label></div>`).join("")}</div><button type="submit" class="btn btn-primary btn-sm justify-self-end">新增项目财务</button></form></div>`, "p-4")}
|
||||
${card(`<h3 class="font-bold text-slate-700 mb-3">项目明细 <span class="text-slate-400 font-normal">(${pfs.length})</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2 text-left font-semibold">客户</th><th class="p-2 text-left font-semibold">类型</th><th class="p-2 text-left font-semibold">状态</th><th class="p-2 text-right font-semibold">签约金额</th>${monthLabels.map(l => `<th class="p-2 text-center font-semibold">${l}<br><span class="text-xs text-slate-400">确收/毛利</span></th>`).join("")}<th class="p-2 text-left font-semibold">销售</th></tr></thead><tbody>${pfs.map(renderPfRow).join("")}</tbody></table></div>`, "p-4")}
|
||||
</div>`;
|
||||
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 + "万" } } } },
|
||||
|
||||
Reference in New Issue
Block a user