564 lines
25 KiB
Python
564 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
月結分析路由模組
|
|
包含:月份總表數據分析展示頁面與 API
|
|
"""
|
|
|
|
from datetime import datetime, timezone, timedelta
|
|
from flask import Blueprint, request, jsonify, render_template
|
|
from auth import login_required
|
|
from sqlalchemy import func, desc, text, case
|
|
|
|
from config import BASE_DIR, SYSTEM_VERSION, DATABASE_TYPE
|
|
from database.manager import DatabaseManager
|
|
from database.models import MonthlySummaryAnalysis
|
|
from services.logger_manager import SystemLogger
|
|
|
|
|
|
def get_group_concat_sql():
|
|
"""根據資料庫類型回傳適當的聚合函數 SQL"""
|
|
if DATABASE_TYPE == 'postgresql':
|
|
return """
|
|
SELECT
|
|
STRING_AGG(DISTINCT year::TEXT, ',') as years,
|
|
STRING_AGG(DISTINCT month::TEXT, ',') as months,
|
|
STRING_AGG(DISTINCT division, ',') as divisions,
|
|
STRING_AGG(DISTINCT pm_name, ',') as pms,
|
|
STRING_AGG(DISTINCT area_name, ',') as areas,
|
|
STRING_AGG(DISTINCT vendor_name, ',') as vendors,
|
|
STRING_AGG(DISTINCT trade_type, ',') as trades
|
|
FROM monthly_summary_analysis
|
|
"""
|
|
else:
|
|
return """
|
|
SELECT
|
|
GROUP_CONCAT(DISTINCT year) as years,
|
|
GROUP_CONCAT(DISTINCT month) as months,
|
|
GROUP_CONCAT(DISTINCT division) as divisions,
|
|
GROUP_CONCAT(DISTINCT pm_name) as pms,
|
|
GROUP_CONCAT(DISTINCT area_name) as areas,
|
|
GROUP_CONCAT(DISTINCT vendor_name) as vendors,
|
|
GROUP_CONCAT(DISTINCT trade_type) as trades
|
|
FROM monthly_summary_analysis
|
|
"""
|
|
|
|
# 時區設定
|
|
TAIPEI_TZ = timezone(timedelta(hours=8))
|
|
|
|
# Logger
|
|
sys_log = SystemLogger("MonthlyRoutes").get_logger()
|
|
|
|
# Blueprint 定義
|
|
monthly_bp = Blueprint('monthly', __name__)
|
|
|
|
|
|
def _split_csv_param(value):
|
|
if not value:
|
|
return []
|
|
return [item.strip() for item in value.split(',') if item.strip()]
|
|
|
|
|
|
def _apply_monthly_filters(query, *, year=None, month=None, division=None, pm_name=None,
|
|
brand_name=None, vendor_name=None, area_name=None,
|
|
trade_type=None, ignore_year=False):
|
|
if year and not ignore_year:
|
|
query = query.filter(MonthlySummaryAnalysis.year == year)
|
|
if month:
|
|
query = query.filter(MonthlySummaryAnalysis.month == month)
|
|
if division:
|
|
query = query.filter(MonthlySummaryAnalysis.division == division)
|
|
if pm_name:
|
|
query = query.filter(MonthlySummaryAnalysis.pm_name == pm_name)
|
|
if brand_name:
|
|
query = query.filter(MonthlySummaryAnalysis.brand_name == brand_name)
|
|
if vendor_name:
|
|
query = query.filter(MonthlySummaryAnalysis.vendor_name == vendor_name)
|
|
area_values = _split_csv_param(area_name)
|
|
if len(area_values) > 1:
|
|
query = query.filter(MonthlySummaryAnalysis.area_name.in_(area_values))
|
|
elif len(area_values) == 1:
|
|
query = query.filter(MonthlySummaryAnalysis.area_name == area_values[0])
|
|
if trade_type:
|
|
query = query.filter(MonthlySummaryAnalysis.trade_type == trade_type)
|
|
return query
|
|
|
|
|
|
# ==========================================
|
|
# 頁面路由
|
|
# ==========================================
|
|
|
|
@monthly_bp.route('/monthly_summary_analysis')
|
|
@login_required
|
|
def monthly_summary_analysis_page():
|
|
"""月份總表數據分析展示頁 (Phase 9)"""
|
|
return render_template('monthly_summary_analysis.html',
|
|
datetime_now=datetime.now(TAIPEI_TZ).strftime('%Y-%m-%d %H:%M:%S'),
|
|
system_version=SYSTEM_VERSION,
|
|
active_page='monthly')
|
|
|
|
|
|
# ==========================================
|
|
# API 路由
|
|
# ==========================================
|
|
|
|
@monthly_bp.route('/api/monthly_summary_trend')
|
|
@login_required
|
|
def get_monthly_summary_trend():
|
|
"""API: 取得月份總表輕量趨勢資料。"""
|
|
year = request.args.get('year', type=int)
|
|
month = request.args.get('month', type=int)
|
|
division = request.args.get('division')
|
|
pm_name = request.args.get('pm_name')
|
|
brand_name = request.args.get('brand_name')
|
|
vendor_name = request.args.get('vendor')
|
|
area_name = request.args.get('area_name')
|
|
trade_type = request.args.get('trade_type')
|
|
|
|
db = DatabaseManager()
|
|
session = db.get_session()
|
|
try:
|
|
trend_query = session.query(
|
|
MonthlySummaryAnalysis.year,
|
|
MonthlySummaryAnalysis.month,
|
|
func.sum(MonthlySummaryAnalysis.sales_amt_curr).label('sales')
|
|
).group_by(
|
|
MonthlySummaryAnalysis.year, MonthlySummaryAnalysis.month
|
|
).order_by(MonthlySummaryAnalysis.year, MonthlySummaryAnalysis.month)
|
|
|
|
trend_query = _apply_monthly_filters(
|
|
trend_query,
|
|
year=year,
|
|
month=month,
|
|
division=division,
|
|
pm_name=pm_name,
|
|
brand_name=brand_name,
|
|
vendor_name=vendor_name,
|
|
area_name=area_name,
|
|
trade_type=trade_type,
|
|
)
|
|
trend_results = trend_query.all()
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'trend': [
|
|
{'date': f"{r.year}/{r.month}", 'sales': int(r.sales or 0)}
|
|
for r in trend_results
|
|
],
|
|
})
|
|
except Exception as e:
|
|
sys_log.error(f"取得月份總表趨勢資料失敗: {e}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
@monthly_bp.route('/api/monthly_summary_data')
|
|
@login_required
|
|
def get_monthly_summary_data():
|
|
"""API: 取得月份總表數據與分析指標 (Phase 9)"""
|
|
year = request.args.get('year', type=int)
|
|
month = request.args.get('month', type=int)
|
|
division = request.args.get('division')
|
|
pm_name = request.args.get('pm_name')
|
|
brand_name = request.args.get('brand_name')
|
|
vendor_name = request.args.get('vendor')
|
|
area_name = request.args.get('area_name')
|
|
trade_type = request.args.get('trade_type')
|
|
limit = request.args.get('limit', default=1000, type=int)
|
|
|
|
db = DatabaseManager()
|
|
session = db.get_session()
|
|
try:
|
|
# 基礎查詢
|
|
query = session.query(MonthlySummaryAnalysis)
|
|
|
|
# 套用過濾
|
|
if year:
|
|
query = query.filter(MonthlySummaryAnalysis.year == year)
|
|
if month:
|
|
query = query.filter(MonthlySummaryAnalysis.month == month)
|
|
if division:
|
|
query = query.filter(MonthlySummaryAnalysis.division == division)
|
|
if pm_name:
|
|
query = query.filter(MonthlySummaryAnalysis.pm_name == pm_name)
|
|
if brand_name:
|
|
query = query.filter(MonthlySummaryAnalysis.brand_name == brand_name)
|
|
if vendor_name:
|
|
query = query.filter(MonthlySummaryAnalysis.vendor_name == vendor_name)
|
|
if area_name:
|
|
if ',' in area_name:
|
|
query = query.filter(MonthlySummaryAnalysis.area_name.in_(area_name.split(',')))
|
|
else:
|
|
query = query.filter(MonthlySummaryAnalysis.area_name == area_name)
|
|
if trade_type:
|
|
query = query.filter(MonthlySummaryAnalysis.trade_type == trade_type)
|
|
|
|
# 取得統計數據 (KPIs)
|
|
kpi_query = session.query(
|
|
func.sum(MonthlySummaryAnalysis.sales_amt_curr).label('total_sales'),
|
|
func.sum(MonthlySummaryAnalysis.sales_amt_prev).label('total_sales_prev'),
|
|
func.sum(MonthlySummaryAnalysis.sales_amt_yoa).label('total_sales_yoa'),
|
|
func.sum(MonthlySummaryAnalysis.profit_amt_curr).label('total_profit'),
|
|
func.sum(MonthlySummaryAnalysis.sales_vol_curr).label('total_vol'),
|
|
func.sum(MonthlySummaryAnalysis.views_curr).label('total_views')
|
|
)
|
|
|
|
# 同樣套用過濾到 KPI
|
|
if year:
|
|
kpi_query = kpi_query.filter(MonthlySummaryAnalysis.year == year)
|
|
if month:
|
|
kpi_query = kpi_query.filter(MonthlySummaryAnalysis.month == month)
|
|
if division:
|
|
kpi_query = kpi_query.filter(MonthlySummaryAnalysis.division == division)
|
|
if pm_name:
|
|
kpi_query = kpi_query.filter(MonthlySummaryAnalysis.pm_name == pm_name)
|
|
if brand_name:
|
|
kpi_query = kpi_query.filter(MonthlySummaryAnalysis.brand_name == brand_name)
|
|
if vendor_name:
|
|
kpi_query = kpi_query.filter(MonthlySummaryAnalysis.vendor_name == vendor_name)
|
|
if area_name:
|
|
if ',' in area_name:
|
|
kpi_query = kpi_query.filter(MonthlySummaryAnalysis.area_name.in_(area_name.split(',')))
|
|
else:
|
|
kpi_query = kpi_query.filter(MonthlySummaryAnalysis.area_name == area_name)
|
|
if trade_type:
|
|
kpi_query = kpi_query.filter(MonthlySummaryAnalysis.trade_type == trade_type)
|
|
|
|
kpi_res = kpi_query.one()
|
|
|
|
# 取得總筆數與月數
|
|
total_rows = session.query(func.count(MonthlySummaryAnalysis.id))
|
|
total_months_query = session.query(
|
|
MonthlySummaryAnalysis.year, MonthlySummaryAnalysis.month
|
|
).distinct()
|
|
|
|
if year:
|
|
total_rows = total_rows.filter(MonthlySummaryAnalysis.year == year)
|
|
total_months_query = total_months_query.filter(MonthlySummaryAnalysis.year == year)
|
|
if month:
|
|
total_rows = total_rows.filter(MonthlySummaryAnalysis.month == month)
|
|
|
|
total_rows = total_rows.scalar()
|
|
total_months = total_months_query.count()
|
|
|
|
# 取得趨勢數據 (按月加總)
|
|
trend_query = session.query(
|
|
MonthlySummaryAnalysis.year,
|
|
MonthlySummaryAnalysis.month,
|
|
func.sum(MonthlySummaryAnalysis.sales_amt_curr).label('sales')
|
|
).group_by(
|
|
MonthlySummaryAnalysis.year, MonthlySummaryAnalysis.month
|
|
).order_by(MonthlySummaryAnalysis.year, MonthlySummaryAnalysis.month)
|
|
|
|
if division:
|
|
trend_query = trend_query.filter(MonthlySummaryAnalysis.division == division)
|
|
if pm_name:
|
|
trend_query = trend_query.filter(MonthlySummaryAnalysis.pm_name == pm_name)
|
|
if brand_name:
|
|
trend_query = trend_query.filter(MonthlySummaryAnalysis.brand_name == brand_name)
|
|
if vendor_name:
|
|
trend_query = trend_query.filter(MonthlySummaryAnalysis.vendor_name == vendor_name)
|
|
if area_name:
|
|
if ',' in area_name:
|
|
trend_query = trend_query.filter(MonthlySummaryAnalysis.area_name.in_(area_name.split(',')))
|
|
else:
|
|
trend_query = trend_query.filter(MonthlySummaryAnalysis.area_name == area_name)
|
|
if trade_type:
|
|
trend_query = trend_query.filter(MonthlySummaryAnalysis.trade_type == trade_type)
|
|
|
|
# 取得排行榜 (Top 10 Brands)
|
|
rank_query = session.query(
|
|
MonthlySummaryAnalysis.brand_name,
|
|
func.sum(MonthlySummaryAnalysis.sales_amt_curr).label('sales')
|
|
).group_by(MonthlySummaryAnalysis.brand_name)
|
|
|
|
if year:
|
|
rank_query = rank_query.filter(MonthlySummaryAnalysis.year == year)
|
|
if month:
|
|
rank_query = rank_query.filter(MonthlySummaryAnalysis.month == month)
|
|
if division:
|
|
rank_query = rank_query.filter(MonthlySummaryAnalysis.division == division)
|
|
if pm_name:
|
|
rank_query = rank_query.filter(MonthlySummaryAnalysis.pm_name == pm_name)
|
|
if brand_name:
|
|
rank_query = rank_query.filter(MonthlySummaryAnalysis.brand_name == brand_name)
|
|
if vendor_name:
|
|
rank_query = rank_query.filter(MonthlySummaryAnalysis.vendor_name == vendor_name)
|
|
if area_name:
|
|
if ',' in area_name:
|
|
rank_query = rank_query.filter(MonthlySummaryAnalysis.area_name.in_(area_name.split(',')))
|
|
else:
|
|
rank_query = rank_query.filter(MonthlySummaryAnalysis.area_name == area_name)
|
|
if trade_type:
|
|
rank_query = rank_query.filter(MonthlySummaryAnalysis.trade_type == trade_type)
|
|
|
|
rank_query = rank_query.order_by(desc('sales')).limit(10)
|
|
|
|
# 取得明細資料
|
|
rows_query = query.order_by(
|
|
MonthlySummaryAnalysis.year.desc(),
|
|
MonthlySummaryAnalysis.month.desc(),
|
|
MonthlySummaryAnalysis.sales_amt_curr.desc()
|
|
).limit(limit)
|
|
|
|
# --- 進階分析子查詢 (Phase 17) ---
|
|
def apply_filters(q, ignore_year=False):
|
|
if year and not ignore_year:
|
|
q = q.filter(MonthlySummaryAnalysis.year == year)
|
|
if month:
|
|
q = q.filter(MonthlySummaryAnalysis.month == month)
|
|
if division:
|
|
q = q.filter(MonthlySummaryAnalysis.division == division)
|
|
if pm_name:
|
|
q = q.filter(MonthlySummaryAnalysis.pm_name == pm_name)
|
|
if brand_name:
|
|
q = q.filter(MonthlySummaryAnalysis.brand_name == brand_name)
|
|
if vendor_name:
|
|
q = q.filter(MonthlySummaryAnalysis.vendor_name == vendor_name)
|
|
if area_name:
|
|
if ',' in area_name:
|
|
q = q.filter(MonthlySummaryAnalysis.area_name.in_(area_name.split(',')))
|
|
else:
|
|
q = q.filter(MonthlySummaryAnalysis.area_name == area_name)
|
|
if trade_type:
|
|
q = q.filter(MonthlySummaryAnalysis.trade_type == trade_type)
|
|
return q
|
|
|
|
# 廠商排行 (Top 20, 分年度)
|
|
vendor_rank_q = session.query(
|
|
MonthlySummaryAnalysis.vendor_name,
|
|
func.sum(MonthlySummaryAnalysis.sales_amt_curr).label('sales'),
|
|
func.sum(case((MonthlySummaryAnalysis.year == 2024, MonthlySummaryAnalysis.sales_amt_curr), else_=0)).label('sales_2024'),
|
|
func.sum(case((MonthlySummaryAnalysis.year == 2025, MonthlySummaryAnalysis.sales_amt_curr), else_=0)).label('sales_2025'),
|
|
func.sum(MonthlySummaryAnalysis.profit_amt_curr).label('profit'),
|
|
func.sum(case((MonthlySummaryAnalysis.year == 2024, MonthlySummaryAnalysis.profit_amt_curr), else_=0)).label('profit_2024'),
|
|
func.sum(case((MonthlySummaryAnalysis.year == 2025, MonthlySummaryAnalysis.profit_amt_curr), else_=0)).label('profit_2025'),
|
|
).group_by(MonthlySummaryAnalysis.vendor_name)
|
|
|
|
vendor_rank_q = apply_filters(vendor_rank_q, ignore_year=True)
|
|
vendor_rank_q = vendor_rank_q.order_by(desc('sales')).limit(20)
|
|
|
|
# 區域分佈 (按 area_name, Top 12, 分年度)
|
|
div_dist_q = session.query(
|
|
MonthlySummaryAnalysis.area_name,
|
|
func.sum(MonthlySummaryAnalysis.sales_amt_curr).label('sales'),
|
|
func.sum(case((MonthlySummaryAnalysis.year == 2024, MonthlySummaryAnalysis.sales_amt_curr), else_=0)).label('sales_2024'),
|
|
func.sum(case((MonthlySummaryAnalysis.year == 2025, MonthlySummaryAnalysis.sales_amt_curr), else_=0)).label('sales_2025')
|
|
).group_by(MonthlySummaryAnalysis.area_name)
|
|
|
|
div_dist_q = apply_filters(div_dist_q, ignore_year=True)
|
|
div_dist_q = div_dist_q.order_by(desc('sales')).limit(12)
|
|
|
|
# 價格帶貢獻 (分年度)
|
|
price_cont_q = session.query(
|
|
case(
|
|
(MonthlySummaryAnalysis.unit_price < 500, '0-499'),
|
|
(MonthlySummaryAnalysis.unit_price < 1000, '500-999'),
|
|
(MonthlySummaryAnalysis.unit_price < 2000, '1,000-1,999'),
|
|
(MonthlySummaryAnalysis.unit_price < 5000, '2,000-4,999'),
|
|
(MonthlySummaryAnalysis.unit_price < 10000, '5,000-9,999'),
|
|
else_='10,000+'
|
|
).label('price_range'),
|
|
func.sum(MonthlySummaryAnalysis.sales_amt_curr).label('sales'),
|
|
func.sum(case((MonthlySummaryAnalysis.year == 2024, MonthlySummaryAnalysis.sales_amt_curr), else_=0)).label('sales_2024'),
|
|
func.sum(case((MonthlySummaryAnalysis.year == 2025, MonthlySummaryAnalysis.sales_amt_curr), else_=0)).label('sales_2025')
|
|
).group_by('price_range')
|
|
price_cont_q = apply_filters(price_cont_q, ignore_year=True)
|
|
|
|
# BCG 矩陣 (品牌 x 區域)
|
|
bcg_q = session.query(
|
|
MonthlySummaryAnalysis.brand_name,
|
|
MonthlySummaryAnalysis.area_name,
|
|
func.sum(MonthlySummaryAnalysis.sales_vol_curr).label('vol'),
|
|
func.sum(MonthlySummaryAnalysis.sales_amt_curr).label('sales'),
|
|
func.sum(MonthlySummaryAnalysis.profit_amt_curr).label('profit')
|
|
).group_by(MonthlySummaryAnalysis.brand_name, MonthlySummaryAnalysis.area_name)\
|
|
.having(func.sum(MonthlySummaryAnalysis.sales_amt_curr) > 0)
|
|
|
|
bcg_q = apply_filters(bcg_q)
|
|
bcg_q = bcg_q.order_by(desc('sales')).limit(100)
|
|
|
|
# 熱力圖 (月份 x 分類)
|
|
div_dist_results = div_dist_q.all()
|
|
top_12_areas = [r.area_name for r in div_dist_results]
|
|
|
|
heatmap_q = session.query(
|
|
MonthlySummaryAnalysis.year,
|
|
MonthlySummaryAnalysis.month,
|
|
MonthlySummaryAnalysis.area_name,
|
|
func.sum(MonthlySummaryAnalysis.sales_amt_curr).label('sales')
|
|
).filter(MonthlySummaryAnalysis.area_name.in_(top_12_areas))\
|
|
.group_by(MonthlySummaryAnalysis.year, MonthlySummaryAnalysis.month, MonthlySummaryAnalysis.area_name)\
|
|
.order_by(MonthlySummaryAnalysis.year, MonthlySummaryAnalysis.month)
|
|
heatmap_q = apply_filters(heatmap_q, ignore_year=True)
|
|
|
|
# Highlights (Top 3)
|
|
def get_highlights_q(metric_col):
|
|
q = session.query(MonthlySummaryAnalysis.brand_name, func.sum(metric_col).label('val'))
|
|
q = apply_filters(q)
|
|
q = q.group_by(MonthlySummaryAnalysis.brand_name).order_by(desc('val')).limit(3)
|
|
return q
|
|
|
|
rev_top_q = get_highlights_q(MonthlySummaryAnalysis.sales_amt_curr)
|
|
profit_top_q = get_highlights_q(MonthlySummaryAnalysis.profit_amt_curr)
|
|
vol_top_q = get_highlights_q(MonthlySummaryAnalysis.sales_vol_curr)
|
|
|
|
# 區域排行
|
|
area_rank_q = session.query(
|
|
MonthlySummaryAnalysis.area_name,
|
|
func.sum(MonthlySummaryAnalysis.sales_amt_curr).label('sales'),
|
|
func.sum(case((MonthlySummaryAnalysis.year == 2024, MonthlySummaryAnalysis.sales_amt_curr), else_=0)).label('sales_2024'),
|
|
func.sum(case((MonthlySummaryAnalysis.year == 2025, MonthlySummaryAnalysis.sales_amt_curr), else_=0)).label('sales_2025')
|
|
).group_by(MonthlySummaryAnalysis.area_name)
|
|
|
|
area_rank_q = apply_filters(area_rank_q, ignore_year=True)
|
|
area_rank_q = area_rank_q.order_by(desc('sales'))
|
|
|
|
# 年度對比趨勢
|
|
yoy_trend_q = session.query(
|
|
MonthlySummaryAnalysis.year,
|
|
MonthlySummaryAnalysis.month,
|
|
func.sum(MonthlySummaryAnalysis.sales_amt_curr).label('sales_curr'),
|
|
func.sum(MonthlySummaryAnalysis.sales_amt_yoa).label('sales_yoa')
|
|
)
|
|
yoy_trend_q = apply_filters(yoy_trend_q)
|
|
yoy_trend_q = yoy_trend_q.group_by(
|
|
MonthlySummaryAnalysis.year, MonthlySummaryAnalysis.month
|
|
).order_by(MonthlySummaryAnalysis.year, MonthlySummaryAnalysis.month)
|
|
|
|
rows = []
|
|
for r in rows_query.all():
|
|
rows.append({
|
|
'year': r.year,
|
|
'month': r.month,
|
|
'division': r.division,
|
|
'pm_name': r.pm_name,
|
|
'area_name': r.area_name,
|
|
'brand_name': r.brand_name,
|
|
'vendor_name': r.vendor_name,
|
|
'trade_type': r.trade_type,
|
|
'sales_amt_curr': r.sales_amt_curr,
|
|
'sales_amt_yoa': r.sales_amt_yoa,
|
|
'sales_vol_curr': r.sales_vol_curr,
|
|
'profit_amt_curr': r.profit_amt_curr,
|
|
'views_curr': r.views_curr
|
|
})
|
|
|
|
# 使用單一 SQL 查詢取得所有不重複的維度列表 (支援 PostgreSQL/SQLite)
|
|
with db.engine.connect() as conn:
|
|
filters_result = conn.execute(text(get_group_concat_sql())).fetchone()
|
|
|
|
years_list = [int(x) for x in (filters_result[0] or '').split(',') if x]
|
|
months_list = [int(x) for x in (filters_result[1] or '').split(',') if x]
|
|
divisions_list = [x for x in (filters_result[2] or '').split(',') if x]
|
|
pms_list = [x for x in (filters_result[3] or '').split(',') if x]
|
|
areas_list = [x for x in (filters_result[4] or '').split(',') if x]
|
|
vendors_list = [x for x in (filters_result[5] or '').split(',') if x]
|
|
trades_list = [x for x in (filters_result[6] or '').split(',') if x]
|
|
|
|
# 預先執行所有查詢
|
|
area_rank_results = area_rank_q.all()
|
|
vendor_rank_results = vendor_rank_q.all()
|
|
price_cont_results = price_cont_q.all()
|
|
bcg_results = bcg_q.all()
|
|
heatmap_results = heatmap_q.all()
|
|
trend_results = trend_query.all()
|
|
yoy_trend_results = yoy_trend_q.all()
|
|
rank_results = rank_query.all()
|
|
rev_top_results = rev_top_q.all()
|
|
profit_top_results = profit_top_q.all()
|
|
vol_top_results = vol_top_q.all()
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'total_rows': total_rows,
|
|
'total_months': total_months,
|
|
'kpis': {
|
|
'sales': int(kpi_res.total_sales or 0),
|
|
'sales_prev': int(kpi_res.total_sales_prev or 0),
|
|
'sales_yoa': int(kpi_res.total_sales_yoa or 0),
|
|
'profit': int(kpi_res.total_profit or 0),
|
|
'vol': int(kpi_res.total_vol or 0),
|
|
'views': int(kpi_res.total_views or 0),
|
|
'margin': round((kpi_res.total_profit / kpi_res.total_sales * 100), 2) if kpi_res.total_sales and kpi_res.total_profit else 0
|
|
},
|
|
'trend': [{'date': f"{r.year}/{r.month}", 'sales': int(r.sales or 0)} for r in trend_results],
|
|
'yoy_trend': [{'date': f"{r.year}/{r.month}", 'curr': int(r.sales_curr or 0), 'yoa': int(r.sales_yoa or 0)} for r in yoy_trend_results],
|
|
'rankings': [{'brand': r.brand_name, 'sales': int(r.sales or 0)} for r in rank_results],
|
|
'area_ranking': [
|
|
{
|
|
'name': r.area_name,
|
|
'sales': int(r.sales or 0),
|
|
'sales_2024': int(r.sales_2024 or 0),
|
|
'sales_2025': int(r.sales_2025 or 0)
|
|
}
|
|
for r in area_rank_results
|
|
],
|
|
'vendor_ranking': [
|
|
{
|
|
'name': r.vendor_name,
|
|
'sales': int(r.sales or 0),
|
|
'sales_2024': int(r.sales_2024 or 0),
|
|
'sales_2025': int(r.sales_2025 or 0),
|
|
'profit': int(r.profit or 0),
|
|
'profit_2024': int(r.profit_2024 or 0),
|
|
'profit_2025': int(r.profit_2025 or 0),
|
|
'margin': round((r.profit / r.sales * 100), 2) if r.sales and r.profit else 0
|
|
}
|
|
for r in vendor_rank_results
|
|
],
|
|
'division_dist': [
|
|
{
|
|
'name': r.area_name,
|
|
'value': int(r.sales or 0),
|
|
'sales_2024': int(r.sales_2024 or 0),
|
|
'sales_2025': int(r.sales_2025 or 0)
|
|
}
|
|
for r in div_dist_results
|
|
],
|
|
'price_contribution': [
|
|
{
|
|
'range': r.price_range,
|
|
'sales': int(r.sales or 0),
|
|
'sales_2024': int(r.sales_2024 or 0),
|
|
'sales_2025': int(r.sales_2025 or 0)
|
|
}
|
|
for r in price_cont_results
|
|
],
|
|
'bcg_data': [
|
|
{
|
|
'name': f"{r.brand_name}-{r.area_name}",
|
|
'qty': int(r.vol or 0),
|
|
'margin': round((r.profit / r.sales * 100), 2) if r.sales and r.profit else 0,
|
|
'sales': int(r.sales or 0)
|
|
}
|
|
for r in bcg_results
|
|
],
|
|
'heatmap_data': [
|
|
{'year': r.year, 'month': r.month, 'category': r.area_name, 'sales': int(r.sales or 0)}
|
|
for r in heatmap_results
|
|
],
|
|
'highlights': {
|
|
'rev_top': [{'name': r.brand_name, 'value': int(r.val or 0)} for r in rev_top_results],
|
|
'profit_top': [{'name': r.brand_name, 'value': int(r.val or 0)} for r in profit_top_results],
|
|
'vol_top': [{'name': r.brand_name, 'value': int(r.val or 0)} for r in vol_top_results]
|
|
},
|
|
'filters': {
|
|
'years': sorted(years_list, reverse=True),
|
|
'months': sorted(months_list),
|
|
'divisions': sorted(divisions_list),
|
|
'pms': sorted(pms_list),
|
|
'areas': sorted(areas_list),
|
|
'vendors': sorted(vendors_list),
|
|
'trades': sorted(trades_list)
|
|
},
|
|
'rows': rows
|
|
})
|
|
|
|
except Exception as e:
|
|
sys_log.error(f"取得月份總表數據失敗: {e}")
|
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
|
finally:
|
|
session.close()
|