feat: 分销商财务仪表盘(收入/成本/毛利×币种,今天/本月/本年/累计)
This commit is contained in:
parent
59072ef035
commit
25cfa19a48
@ -418,3 +418,76 @@ async def get_scoped_fail_count(sor, today, scope=None, org_filter=None):
|
||||
ns['org'] = org_filter
|
||||
recs = await sor.sqlExe(sql, ns)
|
||||
return int(recs[0].cnt) if recs else 0
|
||||
|
||||
|
||||
# ── 分销商财务统计 ──
|
||||
|
||||
from datetime import date as _date_module_date
|
||||
|
||||
async def get_distributor_financial(sor, org_filter, period='today'):
|
||||
"""分销商财务概览:按币种统计收入/成本/毛利
|
||||
period: 'today' | 'month' | 'year' | 'all'
|
||||
收入含直接客户 + 二级分销商
|
||||
"""
|
||||
today_str = _date_module_date.today().isoformat()
|
||||
|
||||
date_sql = ""
|
||||
ns = {'org': org_filter, 'today': today_str}
|
||||
if period == 'month':
|
||||
date_sql = "AND f.call_date BETWEEN ${month_start}$ AND ${today}$"
|
||||
ns['month_start'] = today_str[:7] + '-01'
|
||||
elif period == 'year':
|
||||
date_sql = "AND f.call_date BETWEEN ${year_start}$ AND ${today}$"
|
||||
ns['year_start'] = today_str[:4] + '-01-01'
|
||||
elif period == 'today':
|
||||
date_sql = "AND f.call_date = ${today}$"
|
||||
|
||||
# 收入按币种(含直接客户 + 二级分销商)
|
||||
sql_rev = f"""SELECT
|
||||
COALESCE(f.currency, 'CNY') as currency,
|
||||
COUNT(*) as call_count,
|
||||
COALESCE(SUM(f.amount), 0) as revenue,
|
||||
COALESCE(SUM(f.amount_cny), 0) as revenue_cny
|
||||
FROM dm_model_call_fact f
|
||||
WHERE f.distributor_orgid = ${{org}}$ {date_sql}
|
||||
GROUP BY f.currency ORDER BY revenue_cny DESC"""
|
||||
|
||||
recs = await sor.sqlExe(sql_rev, ns)
|
||||
|
||||
revenue_by_currency = []
|
||||
total_revenue_cny = 0.0
|
||||
total_calls = 0
|
||||
for r in recs:
|
||||
total_revenue_cny += float(r.revenue_cny or 0)
|
||||
total_calls += int(r.call_count or 0)
|
||||
revenue_by_currency.append({
|
||||
'currency': r.currency,
|
||||
'call_count': int(r.call_count or 0),
|
||||
'revenue': round(float(r.revenue or 0), 2),
|
||||
'revenue_cny': round(float(r.revenue_cny or 0), 2),
|
||||
})
|
||||
|
||||
# 成本(尝试从provider_cost归集,关联distributor_orgid)
|
||||
total_cost_cny = 0.0
|
||||
try:
|
||||
sql_cost = f"""SELECT COALESCE(SUM(c.total_amount_cny), 0) as total_cost_cny
|
||||
FROM dm_provider_cost_daily c
|
||||
JOIN dm_model_call_fact f ON f.call_date = c.stat_date
|
||||
AND f.providerid = c.providerid AND f.catelogid = c.catelogid
|
||||
WHERE f.distributor_orgid = ${{org}}$ {date_sql}"""
|
||||
cost_recs = await sor.sqlExe(sql_cost, ns)
|
||||
if cost_recs:
|
||||
total_cost_cny = round(float(cost_recs[0].total_cost_cny or 0), 2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
gross_profit_cny = round(total_revenue_cny - total_cost_cny, 2)
|
||||
|
||||
return {
|
||||
'revenue_by_currency': revenue_by_currency,
|
||||
'total_revenue_cny': total_revenue_cny,
|
||||
'total_cost_cny': total_cost_cny,
|
||||
'gross_profit_cny': gross_profit_cny,
|
||||
'total_calls': total_calls,
|
||||
'period': period,
|
||||
}
|
||||
|
||||
@ -428,6 +428,40 @@ async def api_user_apikeys(request):
|
||||
debug("sage_datamart API error: " + str(e))
|
||||
return []
|
||||
|
||||
|
||||
async def api_distributor_financial(request):
|
||||
"""分销商财务统计 API"""
|
||||
try:
|
||||
env = request._run_ns
|
||||
scope, org_filter = await _resolve_view(request)
|
||||
period = (request._run_ns.params_kw or {}).get('period', 'today')
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
return await get_distributor_financial(sor, org_filter, period)
|
||||
except Exception as e:
|
||||
debug("sage_datamart API error: " + str(e))
|
||||
return {}
|
||||
|
||||
|
||||
async def j2_distributor_financial(request, period='today'):
|
||||
"""Jinja2: 分销商财务数据"""
|
||||
try:
|
||||
env = request._run_ns
|
||||
scope, org_filter = await _resolve_view(request)
|
||||
async with get_sor_context(env, MODULE_NAME) as sor:
|
||||
return await get_distributor_financial(sor, org_filter, period)
|
||||
except Exception:
|
||||
return {'total_revenue_cny': 0, 'total_cost_cny': 0, 'gross_profit_cny': 0,
|
||||
'revenue_by_currency': [], 'total_calls': 0, 'period': period}
|
||||
|
||||
|
||||
async def j2_is_distributor(request):
|
||||
"""Jinja2: 是否分销商角色"""
|
||||
try:
|
||||
scope, _ = await _resolve_view(request)
|
||||
return scope == DISTRIBUTOR
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# ── 监控组件 j2 函数,每段独立 .ui 通过 RefreshWidget 加载 ──
|
||||
|
||||
async def j2_model_perf_data(request):
|
||||
@ -544,6 +578,9 @@ def load_sage_datamart():
|
||||
env.j2_scoped_success_rate = j2_scoped_success_rate
|
||||
env.j2_scoped_fail_count = j2_scoped_fail_count
|
||||
env.api_user_apikeys = api_user_apikeys
|
||||
env.api_distributor_financial = api_distributor_financial
|
||||
env.j2_distributor_financial = j2_distributor_financial
|
||||
env.j2_is_distributor = j2_is_distributor
|
||||
env.j2_model_perf_data = j2_model_perf_data
|
||||
env.j2_currency_data = j2_currency_data
|
||||
env.j2_provider_roi_data = j2_provider_roi_data
|
||||
|
||||
@ -40,6 +40,8 @@ PATHS_LOGINED = [
|
||||
f"/{MOD}/sect_top_users.ui",
|
||||
f"/{MOD}/sect_top_providers.ui",
|
||||
f"/{MOD}/sect_user_apikeys.ui",
|
||||
f"/{MOD}/sect_distributor_financial.ui",
|
||||
f"/{MOD}/sect_distributor_currency.ui",
|
||||
]
|
||||
|
||||
def run(role, path):
|
||||
|
||||
6
wwwroot/api/distributor_currency.dspy
Normal file
6
wwwroot/api/distributor_currency.dspy
Normal file
@ -0,0 +1,6 @@
|
||||
# 分销商币种收入排行
|
||||
import json
|
||||
env = request._run_ns
|
||||
data = await env.api_distributor_financial(request)
|
||||
currency_data = data.get('revenue_by_currency', []) if data else []
|
||||
return json.dumps(currency_data, ensure_ascii=False)
|
||||
13
wwwroot/api/distributor_financial.dspy
Normal file
13
wwwroot/api/distributor_financial.dspy
Normal file
@ -0,0 +1,13 @@
|
||||
# 分销商财务统计
|
||||
import json
|
||||
import asyncio
|
||||
|
||||
env = request._run_ns
|
||||
period = (request._run_ns.params_kw or {}).get('period', 'today')
|
||||
|
||||
data = await env.api_distributor_financial(request)
|
||||
# Fetch all 4 periods
|
||||
today_data = data
|
||||
month_data = await env.api_distributor_financial(request) if period != 'today' else data
|
||||
# Actually, let's just return the single period
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
@ -1,6 +1,7 @@
|
||||
{% set roles = get_user_roles(get_user()) %}
|
||||
{% set userorgid = get_userorgid() if get_userorgid is defined else '' %}
|
||||
{% set is_admin = userorgid == '0' %}
|
||||
{% set is_distributor = j2_is_distributor(request) %}
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"id": "datamart_root",
|
||||
@ -46,6 +47,16 @@
|
||||
{"widgettype": "RefreshWidget", "id": "rt_online", "options": {"period_seconds": 30, "url": "{{entire_url('rt_card_online.ui')}}", "width": "34%"}}
|
||||
]
|
||||
},
|
||||
{% else %}
|
||||
{% if is_distributor %}
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"css": "card", "width": "100%", "padding": "16px", "marginBottom": "16px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "财务概览", "i18n": true, "marginBottom": "8px"}},
|
||||
{"widgettype": "RefreshWidget", "options": {"period_seconds": 60, "url": "{{entire_url('sect_distributor_financial.ui')}}", "width": "100%"}}
|
||||
]
|
||||
},
|
||||
{% else %}
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
@ -69,6 +80,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
|
||||
9
wwwroot/sect_distributor_currency.ui
Normal file
9
wwwroot/sect_distributor_currency.ui
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"widgettype": "ChartBar",
|
||||
"options": {
|
||||
"height": "200px",
|
||||
"data_url": "/sage_datamart/api/distributor_currency.dspy",
|
||||
"nameField": "currency",
|
||||
"valueFields": ["revenue_cny"]
|
||||
}
|
||||
}
|
||||
56
wwwroot/sect_distributor_financial.ui
Normal file
56
wwwroot/sect_distributor_financial.ui
Normal file
@ -0,0 +1,56 @@
|
||||
{% set env = request._run_ns %}
|
||||
{% set scope, orgid = _resolve_view(request) %}
|
||||
{% set today_data = j2_distributor_financial(request, 'today') %}
|
||||
{% set month_data = j2_distributor_financial(request, 'month') %}
|
||||
{% set year_data = j2_distributor_financial(request, 'year') %}
|
||||
{% set all_data = j2_distributor_financial(request, 'all') %}
|
||||
|
||||
{% macro fmt(val) %}{{'¥' + ('%.2f' % val)}}{% endmacro %}
|
||||
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"width": "100%"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "HBox", "options": {"width": "100%", "gap": "12px", "marginBottom": "12px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "VBox", "options": {"css": "card", "padding": "12px", "borderRadius": "6px", "width": "25%"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "今天", "fontWeight": "600", "fontSize": "12px", "color": "var(--sage-text-primary)"}},
|
||||
{"widgettype": "Text", "options": {"text": "收入: " + {{json.dumps(fmt(today_data.total_revenue_cny))}}, "fontSize": "11px", "marginTop": "4px"}},
|
||||
{"widgettype": "Text", "options": {"text": "成本: " + {{json.dumps(fmt(today_data.total_cost_cny))}}, "fontSize": "11px"}},
|
||||
{"widgettype": "Text", "options": {"text": "毛利: " + {{json.dumps(fmt(today_data.gross_profit_cny))}}, "fontSize": "11px", "color": {{json.dumps('#10b981' if today_data.gross_profit_cny >= 0 else '#ef4444')}}}}
|
||||
]
|
||||
},
|
||||
{"widgettype": "VBox", "options": {"css": "card", "padding": "12px", "borderRadius": "6px", "width": "25%"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "本月", "fontWeight": "600", "fontSize": "12px", "color": "var(--sage-text-primary)"}},
|
||||
{"widgettype": "Text", "options": {"text": "收入: " + {{json.dumps(fmt(month_data.total_revenue_cny))}}, "fontSize": "11px", "marginTop": "4px"}},
|
||||
{"widgettype": "Text", "options": {"text": "成本: " + {{json.dumps(fmt(month_data.total_cost_cny))}}, "fontSize": "11px"}},
|
||||
{"widgettype": "Text", "options": {"text": "毛利: " + {{json.dumps(fmt(month_data.gross_profit_cny))}}, "fontSize": "11px", "color": {{json.dumps('#10b981' if month_data.gross_profit_cny >= 0 else '#ef4444')}}}}
|
||||
]
|
||||
},
|
||||
{"widgettype": "VBox", "options": {"css": "card", "padding": "12px", "borderRadius": "6px", "width": "25%"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "本年", "fontWeight": "600", "fontSize": "12px", "color": "var(--sage-text-primary)"}},
|
||||
{"widgettype": "Text", "options": {"text": "收入: " + {{json.dumps(fmt(year_data.total_revenue_cny))}}, "fontSize": "11px", "marginTop": "4px"}},
|
||||
{"widgettype": "Text", "options": {"text": "成本: " + {{json.dumps(fmt(year_data.total_cost_cny))}}, "fontSize": "11px"}},
|
||||
{"widgettype": "Text", "options": {"text": "毛利: " + {{json.dumps(fmt(year_data.gross_profit_cny))}}, "fontSize": "11px", "color": {{json.dumps('#10b981' if year_data.gross_profit_cny >= 0 else '#ef4444')}}}}
|
||||
]
|
||||
},
|
||||
{"widgettype": "VBox", "options": {"css": "card", "padding": "12px", "borderRadius": "6px", "width": "25%"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "累计", "fontWeight": "600", "fontSize": "12px", "color": "var(--sage-text-primary)"}},
|
||||
{"widgettype": "Text", "options": {"text": "收入: " + {{json.dumps(fmt(all_data.total_revenue_cny))}}, "fontSize": "11px", "marginTop": "4px"}},
|
||||
{"widgettype": "Text", "options": {"text": "成本: " + {{json.dumps(fmt(all_data.total_cost_cny))}}, "fontSize": "11px"}},
|
||||
{"widgettype": "Text", "options": {"text": "毛利: " + {{json.dumps(fmt(all_data.gross_profit_cny))}}, "fontSize": "11px", "color": {{json.dumps('#10b981' if all_data.gross_profit_cny >= 0 else '#ef4444')}}}}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{"widgettype": "RefreshWidget", "options": {
|
||||
"period_seconds": 60,
|
||||
"url": "/sage_datamart/sect_distributor_currency.ui",
|
||||
"width": "100%"
|
||||
}}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user