94 lines
3.9 KiB
Python
94 lines
3.9 KiB
Python
"""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())
|