"""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())