后端: - 新增 expense_data TEXT 列(columns.py迁移) - 新增 data_split 迁移:旧 budget_data cost/paid → expense_data(幂等) - sum_budget cost/paid → sum_expense cost/paid(routes.py/helpers.py) - TABLES 配置加入 expense_data 前端: - 项目详情弹窗Tab:基本信息→确收与回款→费用明细→执行信息→任务管理→活动 - 确收与回款Tab:月份/确收/毛利/回款/备注 - 费用明细Tab:月份/费用类型/应付/已付/备注 - 项目表格三视图(月/季/总)cost/paid来源切换为expense_data - 保存逻辑分两路: budget_data(rev/gross/payment)+expense_data(cost/paid)
127 lines
4.6 KiB
Python
127 lines
4.6 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, expense_data FROM project_finances WHERE tenant=? AND status='已签约'",
|
||
[tenant])
|
||
|
||
parsed_budgets = []
|
||
expense_maps = {}
|
||
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),
|
||
}
|
||
parsed_budgets.append((pf, budget_map))
|
||
|
||
# Parse expense_data for cost/paid
|
||
try:
|
||
ed = json.loads(pf.get("expense_data") or "[]")
|
||
except (json.JSONDecodeError, TypeError):
|
||
ed = []
|
||
em = {}
|
||
for e in ed:
|
||
key = (e.get("month") or "").replace("-", "_")
|
||
em[key] = {
|
||
"cost": float(e.get("cost") or 0),
|
||
"paid": float(e.get("paid") or 0),
|
||
}
|
||
expense_maps[pf.get("id", id(pf))] = em
|
||
|
||
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"]
|
||
em = expense_maps.get(pf.get("id", id(pf)), {})
|
||
e = em.get(key)
|
||
if e:
|
||
cost += e["cost"]
|
||
paid += e["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
|