feat: 总览工作台新增损益/现金流/项目经营详细报表

- 预算总览和发生总览各新增3个tab: 损益详细报表、现金流详细报表、项目经营详细报表
- 12个月卡片布局, 按部门(学术/科普/科研/医患/MCN)展示矩阵
- 后端新增6个聚合API: dept-matrix/cf-matrix/project-matrix x 2
- tab改为finance-tab风格, 与经营月报一致
- 修复项目报表函数未注册导致的加载失败
- 版本号 v1.2.0.26
This commit is contained in:
mac
2026-07-11 14:36:00 +08:00
parent 94b9f25938
commit 356c129668
4 changed files with 696 additions and 36 deletions

View File

@@ -724,6 +724,359 @@ def delete_file(file_id):
conn.close()
# ---------- 总览部门损益矩阵 API ----------
@bp.route("/api/overview/dept-matrix")
@login_required
def overview_dept_matrix():
"""返回各工作台的损益数据矩阵(按年/季/月筛选)"""
conn = db()
_year = int(request.args.get("year", date.today().year))
view = request.args.get("view", "monthly")
# 12个月数组
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_gross": [0]*12, "dept_expense": [0]*12, "profit": [0]*12}}
for tenant in tenants:
# 项目毛利(从 budget_data JSON
pfs = rows(conn, "SELECT budget_data FROM plan_finances WHERE tenant=?", [tenant])
gross = [0]*12
for pf in pfs:
try:
bd = json.loads(pf.get("budget_data") or "[]")
except:
bd = []
for b in bd:
m = (b.get("month") or "")[:7]
if m in months:
i = months.index(m)
gross[i] += float(b.get("gross") or 0)
# 部门费用(从 plan_expense_records
expense = [0]*12
exps = rows(conn, "SELECT expense_month, amount FROM plan_expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"])
for e in exps:
m = (e.get("expense_month") or "")[:7]
if m in months:
i = months.index(m)
expense[i] += float(e.get("amount") or 0)
profit = [gross[i] - expense[i] for i in range(12)]
result["data"][tenant] = {
"project_gross": gross,
"dept_expense": expense,
"profit": profit
}
for i in range(12):
result["total"]["project_gross"][i] += gross[i]
result["total"]["dept_expense"][i] += expense[i]
result["total"]["profit"][i] += profit[i]
# 按视图聚合
if view == "quarterly":
def to_quarterly(arr12):
qr = [(0,1,2),(3,4,5),(6,7,8),(9,10,11)]
return [arr12[q[0]]+arr12[q[1]]+arr12[q[2]] for q in qr]
result["months"] = ["Q1","Q2","Q3","Q4"]
for t in tenants:
for key in ["project_gross","dept_expense","profit"]:
result["data"][t][key] = to_quarterly(result["data"][t][key])
for key in ["project_gross","dept_expense","profit"]:
result["total"][key] = to_quarterly(result["total"][key])
elif view == "monthly":
sel_month = request.args.get("month", "")
if sel_month and sel_month in months:
mi = months.index(sel_month)
result["months"] = [sel_month]
for t in tenants:
for key in ["project_gross","dept_expense","profit"]:
result["data"][t][key] = [result["data"][t][key][mi]]
for key in ["project_gross","dept_expense","profit"]:
result["total"][key] = [result["total"][key][mi]]
# else: yearly (default) — keep all 12 months
# Compute annual totals
result["annual"] = {}
for t in tenants:
d = result["data"][t]
result["annual"][t] = {
"project_gross": sum(d["project_gross"]),
"dept_expense": sum(d["dept_expense"]),
"profit": sum(d["profit"])
}
result["annual"]["total"] = {
"project_gross": sum(result["total"]["project_gross"]),
"dept_expense": sum(result["total"]["dept_expense"]),
"profit": sum(result["total"]["profit"])
}
conn.close()
return jsonify(result)
@bp.route("/api/overview/finance-dept-matrix")
@login_required
def overview_finance_dept_matrix():
"""返回各工作台已发生损益数据矩阵"""
conn = db()
_year = date.today().year
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_gross": [0]*12, "dept_expense": [0]*12, "profit": [0]*12}}
for tenant in tenants:
pfs = rows(conn, "SELECT budget_data FROM project_finances WHERE tenant=?", [tenant])
gross = [0]*12
for pf in pfs:
try:
bd = json.loads(pf.get("budget_data") or "[]")
except:
bd = []
for b in bd:
m = (b.get("month") or "")[:7]
if m in months:
i = months.index(m)
gross[i] += float(b.get("gross") or 0)
expense = [0]*12
exps = rows(conn, "SELECT expense_month, amount FROM expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"])
for e in exps:
m = (e.get("expense_month") or "")[:7]
if m in months:
i = months.index(m)
expense[i] += float(e.get("amount") or 0)
profit = [gross[i] - expense[i] for i in range(12)]
result["data"][tenant] = {"project_gross": gross, "dept_expense": expense, "profit": profit}
for i in range(12):
result["total"]["project_gross"][i] += gross[i]
result["total"]["dept_expense"][i] += expense[i]
result["total"]["profit"][i] += profit[i]
result["annual"] = {}
for t in tenants:
d = result["data"][t]
result["annual"][t] = {"project_gross": sum(d["project_gross"]), "dept_expense": sum(d["dept_expense"]), "profit": sum(d["profit"])}
result["annual"]["total"] = {"project_gross": sum(result["total"]["project_gross"]), "dept_expense": sum(result["total"]["dept_expense"]), "profit": sum(result["total"]["profit"])}
conn.close()
return jsonify(result)
@bp.route("/api/overview/dept-cf-matrix")
@login_required
def overview_dept_cf_matrix():
"""返回各工作台预算现金流数据矩阵"""
conn = db()
_year = date.today().year
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_cf": [0]*12, "dept_cf": [0]*12, "bl_cf": [0]*12}}
for tenant in tenants:
pfs = rows(conn, "SELECT budget_data, expense_data FROM plan_finances WHERE tenant=?", [tenant])
project_cf = [0]*12
for pf in pfs:
try: bd = json.loads(pf.get("budget_data") or "[]")
except: bd = []
try: ed = json.loads(pf.get("expense_data") or "[]")
except: ed = []
for b in bd:
m = (b.get("month") or "")[:7]
if m in months:
i = months.index(m)
project_cf[i] += float(b.get("payment") or 0)
for e in ed:
m = (e.get("month") or "")[:7]
if m in months:
i = months.index(m)
project_cf[i] -= float(e.get("paid") or 0)
dept_cf = [0]*12
exps = rows(conn, "SELECT expense_month, incurred_amount FROM plan_expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"])
for e in exps:
m = (e.get("expense_month") or "")[:7]
if m in months:
i = months.index(m)
dept_cf[i] += float(e.get("incurred_amount") or 0)
bl_cf = [project_cf[i] - dept_cf[i] for i in range(12)]
result["data"][tenant] = {"project_cf": project_cf, "dept_cf": dept_cf, "bl_cf": bl_cf}
for i in range(12):
result["total"]["project_cf"][i] += project_cf[i]
result["total"]["dept_cf"][i] += dept_cf[i]
result["total"]["bl_cf"][i] += bl_cf[i]
result["annual"] = {}
for t in tenants:
d = result["data"][t]
result["annual"][t] = {"project_cf": sum(d["project_cf"]), "dept_cf": sum(d["dept_cf"]), "bl_cf": sum(d["bl_cf"])}
result["annual"]["total"] = {"project_cf": sum(result["total"]["project_cf"]), "dept_cf": sum(result["total"]["dept_cf"]), "bl_cf": sum(result["total"]["bl_cf"])}
conn.close()
return jsonify(result)
@bp.route("/api/overview/finance-dept-cf-matrix")
@login_required
def overview_finance_dept_cf_matrix():
"""返回各工作台已发生现金流数据矩阵"""
conn = db()
_year = date.today().year
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_cf": [0]*12, "dept_cf": [0]*12, "bl_cf": [0]*12}}
for tenant in tenants:
pfs = rows(conn, "SELECT budget_data, expense_data FROM project_finances WHERE tenant=?", [tenant])
project_cf = [0]*12
for pf in pfs:
try: bd = json.loads(pf.get("budget_data") or "[]")
except: bd = []
try: ed = json.loads(pf.get("expense_data") or "[]")
except: ed = []
for b in bd:
m = (b.get("month") or "")[:7]
if m in months:
i = months.index(m)
project_cf[i] += float(b.get("payment") or 0)
for e in ed:
m = (e.get("month") or "")[:7]
if m in months:
i = months.index(m)
project_cf[i] -= float(e.get("paid") or 0)
dept_cf = [0]*12
exps = rows(conn, "SELECT expense_month, incurred_amount FROM expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"])
for e in exps:
m = (e.get("expense_month") or "")[:7]
if m in months:
i = months.index(m)
dept_cf[i] += float(e.get("incurred_amount") or 0)
bl_cf = [project_cf[i] - dept_cf[i] for i in range(12)]
result["data"][tenant] = {"project_cf": project_cf, "dept_cf": dept_cf, "bl_cf": bl_cf}
for i in range(12):
result["total"]["project_cf"][i] += project_cf[i]
result["total"]["dept_cf"][i] += dept_cf[i]
result["total"]["bl_cf"][i] += bl_cf[i]
result["annual"] = {}
for t in tenants:
d = result["data"][t]
result["annual"][t] = {"project_cf": sum(d["project_cf"]), "dept_cf": sum(d["dept_cf"]), "bl_cf": sum(d["bl_cf"])}
result["annual"]["total"] = {"project_cf": sum(result["total"]["project_cf"]), "dept_cf": sum(result["total"]["dept_cf"]), "bl_cf": sum(result["total"]["bl_cf"])}
conn.close()
return jsonify(result)
@bp.route("/api/overview/dept-project-matrix")
@login_required
def overview_dept_project_matrix():
"""返回各工作台预算项目经营详情矩阵"""
conn = db()
_year = date.today().year
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
keys = ["sign","revenue","gross","cost","paid","payment","cashflow"]
result = {"tenants": tenants, "months": months, "data": {}, "total": {k: [0]*12 for k in keys}}
for tenant in tenants:
tdata = {k: [0]*12 for k in keys}
pfs = rows(conn, "SELECT sign_amount, sign_month, budget_data, expense_data FROM plan_finances WHERE tenant=?", [tenant])
for pf in pfs:
sm = (pf.get("sign_month") or "")[:7]
if sm in months:
i = months.index(sm)
tdata["sign"][i] += float(pf.get("sign_amount") or 0)
try: bd = json.loads(pf.get("budget_data") or "[]")
except: bd = []
try: ed = json.loads(pf.get("expense_data") or "[]")
except: ed = []
for b in bd:
m = (b.get("month") or "")[:7]
if m in months:
i = months.index(m)
tdata["revenue"][i] += float(b.get("rev") or 0)
tdata["gross"][i] += float(b.get("gross") or 0)
tdata["payment"][i] += float(b.get("payment") or 0)
for e in ed:
m = (e.get("month") or "")[:7]
if m in months:
i = months.index(m)
tdata["cost"][i] += float(e.get("cost") or 0)
tdata["paid"][i] += float(e.get("paid") or 0)
for i in range(12):
tdata["cashflow"][i] = tdata["payment"][i] - tdata["paid"][i]
result["data"][tenant] = tdata
for k in keys:
for i in range(12):
result["total"][k][i] += tdata[k][i]
result["annual"] = {}
for t in tenants:
result["annual"][t] = {k: sum(result["data"][t][k]) for k in keys}
result["annual"]["total"] = {k: sum(result["total"][k]) for k in keys}
conn.close()
return jsonify(result)
@bp.route("/api/overview/finance-dept-project-matrix")
@login_required
def overview_finance_dept_project_matrix():
"""返回各工作台已发生项目经营详情矩阵"""
conn = db()
_year = date.today().year
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
keys = ["sign","revenue","gross","cost","paid","payment","cashflow"]
result = {"tenants": tenants, "months": months, "data": {}, "total": {k: [0]*12 for k in keys}}
for tenant in tenants:
tdata = {k: [0]*12 for k in keys}
pfs = rows(conn, "SELECT sign_amount, sign_month, budget_data, expense_data FROM project_finances WHERE tenant=?", [tenant])
for pf in pfs:
sm = (pf.get("sign_month") or "")[:7]
if sm in months:
i = months.index(sm)
tdata["sign"][i] += float(pf.get("sign_amount") or 0)
try: bd = json.loads(pf.get("budget_data") or "[]")
except: bd = []
try: ed = json.loads(pf.get("expense_data") or "[]")
except: ed = []
for b in bd:
m = (b.get("month") or "")[:7]
if m in months:
i = months.index(m)
tdata["revenue"][i] += float(b.get("rev") or 0)
tdata["gross"][i] += float(b.get("gross") or 0)
tdata["payment"][i] += float(b.get("payment") or 0)
for e in ed:
m = (e.get("month") or "")[:7]
if m in months:
i = months.index(m)
tdata["cost"][i] += float(e.get("cost") or 0)
tdata["paid"][i] += float(e.get("paid") or 0)
for i in range(12):
tdata["cashflow"][i] = tdata["payment"][i] - tdata["paid"][i]
result["data"][tenant] = tdata
for k in keys:
for i in range(12):
result["total"][k][i] += tdata[k][i]
result["annual"] = {}
for t in tenants:
result["annual"][t] = {k: sum(result["data"][t][k]) for k in keys}
result["annual"]["total"] = {k: sum(result["total"][k]) for k in keys}
conn.close()
return jsonify(result)
# ---------- health ----------
@bp.route("/api/health")