Files

94 lines
3.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""KPI计算引擎 v4 — 基于会计科目余额和销售报表"""
import httpx, asyncio, os
from datetime import datetime
from app.database import get_session_local
from app.models import KPIDefinition, KPIValue
ERP_API = "http://127.0.0.1:8300"
ERP_KEY = os.environ.get("ERP_API_KEY", "erp-gateway-key-bhwl-2026")
async def _get(url: str, params: dict = None):
async with httpx.AsyncClient(timeout=20) as c:
r = await c.get(url, headers={"X-API-Key": ERP_KEY}, params=params)
return r.json()
async def calculate_all():
db = get_session_local()()
try:
now = datetime.now()
period = f"{now.year}-{now.month:02d}"
# 1. 从科目余额表取数据(BalanceInfo
balance_data = await _get(f"{ERP_API}/api/v1/query", {"table": "BalanceInfo", "limit": 200})
balances = balance_data.get("data", [])
# 按科目和期间汇总
revenue = 0 # 营业收入 (Act_ID=4)
cost = 0 # 营业成本 (Act_ID=5)
ar_balance = 0 # 应收账款 (Act_ID=3)
inv_balance = 0 # 库存商品 (Act_ID=2)
for b in balances:
aid = b.get("Act_ID")
tot = float(b.get("Act_Tot", 0) or 0)
hap = float(b.get("Act_Hap", 0) or 0)
if aid == 4: # 营业收入(本期发生额更准确)
revenue += hap if hap > 0 else tot
elif aid == 5: # 营业成本
cost += hap if hap > 0 else tot
elif aid == 3: # 应收账款余额
ar_balance = tot
elif aid == 2: # 存货余额
inv_balance = tot
# 2. 从销售总览取数据
summary = await _get(f"{ERP_API}/api/v1/stats/sales-summary", {"year": now.year})
s = summary.get("summary", {})
total_sales = s.get("total_amount", 0)
total_customers = s.get("customer_count", 0)
# 3. 前5客户集中度
top_customers = await _get(f"{ERP_API}/api/v1/stats/customer-top", {"year": now.year, "limit": 5})
top5_amt = sum(c["amount"] for c in top_customers.get("data", []))
top5_ratio = round(top5_amt / total_sales * 100, 1) if total_sales > 0 else 0
# 4. 计算KPI
gross_margin = round((revenue - cost) / revenue * 100, 2) if revenue > 0 else 0
ar_turnover = round(revenue / ar_balance, 2) if ar_balance > 0 else 0
inv_turnover = round(cost / inv_balance, 2) if inv_balance > 0 else 0
kpi_values = {
"SALES_TOTAL": total_sales,
"CUSTOMER_COUNT": total_customers,
"SALES_PROFIT_RATE": gross_margin,
"RECEIVABLE_TURNOVER": ar_turnover,
"TURNOVER_RATE": inv_turnover,
"TOP5_CUSTOMER_RATIO": top5_ratio,
}
for code, value in kpi_values.items():
kpi = db.query(KPIDefinition).filter(KPIDefinition.kpi_code == code).first()
if kpi and (value > 0 or kpi.kpi_code in ("SALES_PROFIT_RATE","RECEIVABLE_TURNOVER","TURNOVER_RATE")):
existing = db.query(KPIValue).filter(
KPIValue.kpi_id == kpi.id,
KPIValue.period == period,
KPIValue.source_type == "erp",
).first()
if not existing:
kv = KPIValue(kpi_id=kpi.id, period=period, actual_value=round(value, 2), source_type="erp", data_status="verified")
db.add(kv)
db.commit()
print(f"✅ KPI计算完成: {period}")
for code, value in kpi_values.items():
kpi_n = db.query(KPIDefinition).filter(KPIDefinition.kpi_code == code).first()
n = kpi_n.kpi_name if kpi_n else code
print(f" {n}: {round(value,2) if value else '-'}")
except Exception as e:
print(f"❌ KPI计算失败: {e}")
finally:
db.close()
if __name__ == "__main__":
asyncio.run(calculate_all())