首页: - 5卡片→表格(行=指标,列=时间),新增已付/现金流/项目利润行,费用行 - 行列对换:8列(年度/季度/季环比/本月/月环比/上季度/上月)×9行 - 新增「部门经营情况」卡片(部门利润/费用/净利) - 趋势图3→4列,新增月度项目利润折线图(indigo) - 命名:利润→项目利润,费用→项目费用,月度利润→月度项目利润 - 卡片改名:经营管理→项目管理,财务概览→业务(项目)财务概览 - 指标列统一颜色,去掉行编号 项目模块: - 利润→项目利润,费用→项目费用,总费用→总项目费用 - 月度回款与费用→月度回款与已付,柱状图→折线图 后端: - 新增paid/cashflow/profit指标聚合(单/多租户) - monthly_finance新增paid字段 - helpers.py修复重复return data
111 lines
4.1 KiB
Python
111 lines
4.1 KiB
Python
# helpers.py — 业务查询辅助函数
|
||
# 依赖:db.py;被 routes.py 和 migrations/seed_data.py 调用
|
||
|
||
import json
|
||
from pathlib import Path
|
||
from db import _exec, rows, one
|
||
|
||
|
||
def add_file_index(conn, module, owner_id, owner_version, category, path, external=True):
|
||
path = Path(path)
|
||
if not path.exists():
|
||
return
|
||
_exec(conn,
|
||
"""INSERT INTO file_assets
|
||
(module,owner_id,owner_version,file_category,file_name,file_type,file_size,file_path,is_external)
|
||
VALUES (?,?,?,?,?,?,?,?,?)""",
|
||
(module, owner_id, owner_version, category, path.name, path.suffix.lower().lstrip("."), path.stat().st_size, str(path), 1 if external else 0),
|
||
)
|
||
|
||
|
||
def attach_common(conn, resource, items):
|
||
"""批量加载 followups 和 files,避免 N+1 查询"""
|
||
if not items:
|
||
return items
|
||
target_map = {"sales": "sales", "proposals": "proposal", "operations": "operation", "products": "product"}
|
||
target_type = target_map.get(resource)
|
||
ids = [item["id"] for item in items]
|
||
|
||
# 批量查 followups(一次性 IN 查询)
|
||
if target_type:
|
||
placeholders = ",".join(["?"] * len(ids))
|
||
all_followups = rows(
|
||
conn,
|
||
f"SELECT * FROM follow_up_records WHERE target_type=? AND target_id IN ({placeholders}) ORDER BY followed_at DESC, id DESC",
|
||
[target_type] + ids,
|
||
)
|
||
followups_by_id = {}
|
||
for fu in all_followups:
|
||
followups_by_id.setdefault(fu["target_id"], []).append(fu)
|
||
for item in items:
|
||
item["followups"] = followups_by_id.get(item["id"], [])
|
||
item["latest_follow_up_record"] = item["followups"][0]["content"] if item["followups"] else ""
|
||
|
||
# 批量查 files(proposals + operations)
|
||
file_modules = {"proposals": "proposal", "operations": "operation"}
|
||
if resource in file_modules:
|
||
module = file_modules[resource]
|
||
placeholders = ",".join(["?"] * len(ids))
|
||
all_files = rows(
|
||
conn,
|
||
f"SELECT * FROM file_assets WHERE module=? AND owner_id IN ({placeholders}) ORDER BY id DESC",
|
||
[module] + ids,
|
||
)
|
||
files_by_id = {}
|
||
for f in all_files:
|
||
files_by_id.setdefault(f["owner_id"], []).append(f)
|
||
for item in items:
|
||
item["files"] = files_by_id.get(item["id"], [])
|
||
|
||
return items
|
||
|
||
|
||
def monthly_finance(conn, tenant="科普·无界"):
|
||
months = [f"2026-{m:02d}" for m in range(1, 13)]
|
||
pfs = rows(conn,
|
||
"SELECT sign_amount, sign_month, status, budget_data FROM project_finances WHERE tenant=? AND status='已签约'",
|
||
[tenant])
|
||
|
||
parsed_budgets = []
|
||
for pf in pfs:
|
||
try:
|
||
budget = json.loads(pf.get("budget_data") or "[]")
|
||
except (json.JSONDecodeError, TypeError):
|
||
budget = []
|
||
budget_map = {}
|
||
for b in budget:
|
||
key = (b.get("month") or "").replace("-", "_")
|
||
budget_map[key] = {
|
||
"rev": float(b.get("rev") or 0),
|
||
"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 = 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)
|
||
b = budget_map.get(key)
|
||
if b:
|
||
revenue += b["rev"]
|
||
gross += b["gross"]
|
||
payment += b["payment"]
|
||
cost += b["cost"]
|
||
paid += b["paid"]
|
||
data.append({
|
||
"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
|