feat(ppt): new_product 30-day tracking report v3.1.0
All checks were successful
CD Pipeline / deploy (push) Successful in 2m35s
All checks were successful
CD Pipeline / deploy (push) Successful in 2m35s
Wave 3.1:新品追蹤報告(PM/採購用) generate_new_product_ppt — 9 頁 - P1 封面:含新品力徽章(強勁/穩健/偏弱/疲弱,依業績佔比)+ 三亮點 - P2 KPI:新品總數/業績/佔比/平均單品業績 + AI 解讀 - P3 新品整體日業績曲線(爬榜軌跡 matplotlib) - P4 新品依品類分佈橫條 - P5-P7 新品 TOP 50(自動分頁) - P8 AI PM 戰術洞察 - P9 附錄 query_new_products(days_recent=30, days_baseline=60) - PostgreSQL CTE:recent EXCEPT early recent = 近 30 天有銷售 early = 31-90 天前有銷售 - 自動回傳:新品 TOP 50 / 子品類分佈 / 日業績曲線 - 含 ANY array 查詢新品集合的整體日業績 _ppt_ai_analysis 加 is_new_prod 分支 - 角色:PM 商品經理 + 採購主管 - 結構:新品力評估 / 明星新品識別 / 品類分佈與機會 / SMART 三層 / 風險預警 - SMART 行動含:加碼 TOP3 / 觀察排名 11-30 / 數據追蹤 - max_tokens 1800 業界基準:新品業績佔比 5-10% 為健康,>8% 強勁,<3% 偏弱 路由: - /ppt new_product 預設近 30 天 - /ppt new_product 14 自訂追蹤天數 Telegram 按鈕:「🆕 新品 30 天追蹤」 bump TEMPLATE_VERSIONS['new_product'] = v3.1.0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1889,6 +1889,7 @@ def _ppt_ai_analysis(prompt_data: str, report_type: str = '') -> str:
|
||||
is_customer = '客戶' in report_type or 'customer' in report_type
|
||||
is_forecast = '檔期前瞻' in report_type or 'forecast' in report_type
|
||||
is_promo_cmp = '多活動' in report_type or 'promo_compare' in report_type
|
||||
is_new_prod = '新品' in report_type or 'new_product' in report_type
|
||||
|
||||
# ── 格式鐵律(所有 prompt 共用後綴)────────────────────────
|
||||
FORMAT_RULES = (
|
||||
@@ -2046,6 +2047,46 @@ def _ppt_ai_analysis(prompt_data: str, report_type: str = '') -> str:
|
||||
+ FORMAT_RULES
|
||||
)
|
||||
max_tokens = 1400
|
||||
elif is_new_prod:
|
||||
sys_instruction = (
|
||||
"你身兼 (1) PM 商品經理(精通新品上架 / 商品生命週期 / SKU 健康度)"
|
||||
"(2) 採購主管(精通選品、新廠商引進、新品扶植)。\n"
|
||||
"你的客戶是 momo PM 與採購團隊,會用本報告做新品扶植加碼、"
|
||||
"曇花一現新品下架、明星新品行銷加碼的決策。\n\n"
|
||||
f"請針對以下{report_type}資料,輸出新品戰術洞察,結構嚴格如下:\n\n"
|
||||
"【新品力評估】(3-4 句)\n"
|
||||
"引用新品數、新品業績、業績佔比,評估新品力等級(強勁 >8% / 穩健 3-8% / "
|
||||
"偏弱 1-3% / 疲弱 <1%);與業界平均(健康電商 5-10%)比較定位;"
|
||||
"若 <3% 必須點明「新品引進不足,將失去成長動能」並建議下季加碼新品開發。\n\n"
|
||||
"【明星新品識別】(3-4 句)\n"
|
||||
"點名 TOP3 明星新品,分析高業績成因(檔期推力 / 品牌力 / 行銷投放 / "
|
||||
"獨家代理);建議哪 1-2 款適合做次月主推 hero SKU;"
|
||||
"若 TOP1 新品業績 >NT$10 萬則建議升格為「常銷主力」加碼資源。\n\n"
|
||||
"【品類分佈與機會】(3-4 句)\n"
|
||||
"新品依品類分佈是否健康(過度集中於單一品類?);"
|
||||
"建議下季應加碼新品的品類(依 2026 趨勢:永續美妝 / 母嬰高端 / "
|
||||
"機能性食品 / 男性保養 / 銀髮保健等);"
|
||||
"識別「新品荒漠」品類(無新品進駐者),建議優先填補。\n\n"
|
||||
"【新品扶植與淘汰建議 — SMART 框架】\n"
|
||||
"■ 立即執行(3 條,✅ 開頭):\n"
|
||||
" ✅ 加碼:對 TOP3 新品(具體商品名)增加首頁版位/廣告預算 +X%,"
|
||||
"預期業績 +Y%,期限:YYYY/MM/DD\n"
|
||||
" ✅ 觀察:對排名 11-30 名新品(具體商品名)2 週後回看,"
|
||||
"若週業績 < NT$Z 則啟動下架評估\n"
|
||||
" ✅ 數據追蹤:建立新品 KPI 儀表板(爬榜速度 / 客單 / 復購率),"
|
||||
"每週自動更新\n"
|
||||
"■ 中期強化(2 條,✅ 開頭):開發新廠商 / 跨品類聯名 / 自有品牌 OEM\n"
|
||||
"■ 長期佈局(1 條,✅ 開頭):建立新品引進 SOP(試銷 30 天 → "
|
||||
"達標升常銷 / 不達標下架)\n\n"
|
||||
"【最大風險與防禦】(2-3 句)\n"
|
||||
"(a) 新品試銷失敗率高 → 建議單一品類下架率 >50% 觸發採購復盤\n"
|
||||
"(b) 新品搶食常銷市場 → 觀察常銷商品銷量是否被新品稀釋\n"
|
||||
"(c) 過度依賴單品爆款 → 建議新品 TOP1 佔新品業績 <30% 為健康\n\n"
|
||||
"要求:每段引用至少 2 個具體數字,全文 800~1000 字,禁用模糊用詞。"
|
||||
+ MARKET_TREND_2026
|
||||
+ FORMAT_RULES
|
||||
)
|
||||
max_tokens = 1800
|
||||
elif is_promo_cmp:
|
||||
sys_instruction = (
|
||||
"你是資深行銷主管(10 年促銷活動策劃實戰經驗)。"
|
||||
@@ -2750,6 +2791,7 @@ def _generate_ppt_cmd(sub_type: str, sub_arg: str, _chat_id: int, target: str,
|
||||
generate_vendor_ppt, generate_period_review_ppt,
|
||||
generate_category_deep_ppt, generate_customer_analytics_ppt,
|
||||
generate_forecast_pre_event_ppt, generate_promo_compare_ppt,
|
||||
generate_new_product_ppt,
|
||||
check_pptx_available
|
||||
)
|
||||
except ImportError:
|
||||
@@ -3274,6 +3316,58 @@ def _generate_ppt_cmd(sub_type: str, sub_arg: str, _chat_id: int, target: str,
|
||||
})
|
||||
return ppt_path
|
||||
|
||||
elif sub_type in ('new_product', 'newproduct', '新品', '新品追蹤'):
|
||||
# /ppt new_product 預設 30 天追蹤
|
||||
# /ppt new_product 14 自訂追蹤天數
|
||||
days = 30
|
||||
if sub_arg and sub_arg.isdigit():
|
||||
days = int(sub_arg)
|
||||
baseline_days = 60
|
||||
|
||||
params = {'report_type': 'new_product', 'days': days}
|
||||
cached, cached_ai = _load_cached_ppt_path_and_analysis('new_product', params)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
mcp_text = ''
|
||||
if not cached_ai:
|
||||
mcp_text = _fetch_mcp_context()
|
||||
|
||||
np_data = query_new_products(days_recent=days, days_baseline=baseline_days)
|
||||
if not np_data.get('found'):
|
||||
raise RuntimeError(f'近 {days} 天無新品(前 {baseline_days} 天無交易但近期有銷售的商品)')
|
||||
|
||||
kpis = np_data.get('kpis', {})
|
||||
top5_str = '\n'.join(
|
||||
f" {i+1}. {p.get('name','')[:30]} ({p.get('category','—')}) — "
|
||||
f"NT${p.get('revenue', 0):,.0f}"
|
||||
for i, p in enumerate(np_data.get('new_products', [])[:5])
|
||||
)
|
||||
sub_str = '\n'.join(
|
||||
f" - {c.get('name','')}: {c.get('sku_count', 0)} 款 / "
|
||||
f"NT${c.get('revenue', 0):,.0f}"
|
||||
for c in np_data.get('sub_categories', [])[:5]
|
||||
)
|
||||
data_summary = (
|
||||
f"【追蹤期間】{np_data.get('period', '')}\n"
|
||||
f"【新品總數】{kpis.get('new_count', 0)} 款\n"
|
||||
f"【新品業績】NT${kpis.get('new_revenue', 0):,.0f}\n"
|
||||
f"【業績佔比】{kpis.get('new_pct', 0):.1f}%(vs 整體 NT${kpis.get('total_revenue', 0):,.0f})\n\n"
|
||||
f"【新品 TOP 5】\n{top5_str}\n\n"
|
||||
f"【新品依品類分佈】\n{sub_str}\n\n"
|
||||
f"【MCP 外部市場情報】\n{mcp_text[:500] if mcp_text else '(無)'}"
|
||||
)
|
||||
ai_text = cached_ai or _ppt_ai_analysis(data_summary, '新品追蹤報告')
|
||||
if not cached_ai and _ppt_needs_fallback(ai_text):
|
||||
ai_text = _ppt_fallback_insight('新品追蹤', data_summary, mcp_text)
|
||||
|
||||
ppt_path = generate_new_product_ppt(np_data, ai_text)
|
||||
_store_ppt_cache('new_product', params, ppt_path, {
|
||||
'report_type': 'new_product', 'parameters': params,
|
||||
'data_summary': data_summary, 'analysis': ai_text, 'mcp': mcp_text,
|
||||
})
|
||||
return ppt_path
|
||||
|
||||
elif sub_type in ('promo_compare', 'promocompare', '促銷比較', '多活動'):
|
||||
# /ppt promo_compare 母親節:2026/05/05-2026/05/14|520:2026/05/18-2026/05/22|618:2026/06/14-2026/06/22
|
||||
# 用 | 分隔多場活動,每場用 : 分 label/dates
|
||||
@@ -4721,6 +4815,123 @@ def query_date_range(start_str: str, end_str: str) -> dict:
|
||||
return {'found': False, 'range': f'{start_str}~{end_str}'}
|
||||
|
||||
|
||||
def query_new_products(days_recent: int = 30, days_baseline: int = 60) -> dict:
|
||||
"""新品追蹤:近 days_recent 天有銷售、過去 days_baseline 天無銷售的商品
|
||||
|
||||
回傳:{
|
||||
period, kpis: {new_count, new_revenue, new_pct, top1_revenue},
|
||||
new_products: [TOP 50 含日銷售軌跡],
|
||||
sub_categories: [新品依品類分佈],
|
||||
daily_total: [{date, new_revenue}], # 新品整體日業績
|
||||
}
|
||||
"""
|
||||
try:
|
||||
with _db().connect() as c:
|
||||
# 主查詢:近 N 天 EXCEPT 早期
|
||||
new_rows = c.execute(text(f"""
|
||||
WITH recent AS (
|
||||
SELECT "商品ID", "商品名稱", "商品分類L1",
|
||||
SUM(CAST("總業績" AS FLOAT)) AS rev,
|
||||
SUM(CAST("數量" AS INTEGER)) AS qty,
|
||||
COUNT(DISTINCT "訂單編號") AS orders,
|
||||
MIN(CAST("日期" AS DATE)) AS first_seen
|
||||
FROM realtime_sales_monthly
|
||||
WHERE CAST("日期" AS DATE) >= CURRENT_DATE - INTERVAL '{days_recent} days'
|
||||
GROUP BY "商品ID", "商品名稱", "商品分類L1"
|
||||
),
|
||||
early AS (
|
||||
SELECT DISTINCT "商品ID"
|
||||
FROM realtime_sales_monthly
|
||||
WHERE CAST("日期" AS DATE) BETWEEN
|
||||
CURRENT_DATE - INTERVAL '{days_recent + days_baseline} days' AND
|
||||
CURRENT_DATE - INTERVAL '{days_recent + 1} days'
|
||||
)
|
||||
SELECT recent.* FROM recent
|
||||
LEFT JOIN early ON recent."商品ID" = early."商品ID"
|
||||
WHERE early."商品ID" IS NULL
|
||||
ORDER BY recent.rev DESC LIMIT 50
|
||||
""")).fetchall()
|
||||
|
||||
# 新品總業績 + 整體業績佔比
|
||||
new_rev_total = sum(float(r[3] or 0) for r in new_rows)
|
||||
total_row = c.execute(text(f"""
|
||||
SELECT COALESCE(SUM(CAST("總業績" AS FLOAT)), 0)
|
||||
FROM realtime_sales_monthly
|
||||
WHERE CAST("日期" AS DATE) >= CURRENT_DATE - INTERVAL '{days_recent} days'
|
||||
""")).fetchone()
|
||||
total_rev = float(total_row[0] or 0)
|
||||
|
||||
# 子品類分佈
|
||||
sub_dist = c.execute(text(f"""
|
||||
WITH recent AS (
|
||||
SELECT "商品ID", "商品分類L1",
|
||||
SUM(CAST("總業績" AS FLOAT)) AS rev
|
||||
FROM realtime_sales_monthly
|
||||
WHERE CAST("日期" AS DATE) >= CURRENT_DATE - INTERVAL '{days_recent} days'
|
||||
GROUP BY "商品ID", "商品分類L1"
|
||||
),
|
||||
early AS (
|
||||
SELECT DISTINCT "商品ID"
|
||||
FROM realtime_sales_monthly
|
||||
WHERE CAST("日期" AS DATE) BETWEEN
|
||||
CURRENT_DATE - INTERVAL '{days_recent + days_baseline} days' AND
|
||||
CURRENT_DATE - INTERVAL '{days_recent + 1} days'
|
||||
)
|
||||
SELECT COALESCE(recent."商品分類L1", '其他') AS cat,
|
||||
COUNT(*) AS sku_count,
|
||||
SUM(recent.rev) AS rev
|
||||
FROM recent
|
||||
LEFT JOIN early ON recent."商品ID" = early."商品ID"
|
||||
WHERE early."商品ID" IS NULL
|
||||
GROUP BY recent."商品分類L1"
|
||||
ORDER BY 3 DESC LIMIT 10
|
||||
""")).fetchall()
|
||||
|
||||
# 新品整體日業績曲線
|
||||
new_ids = [r[0] for r in new_rows]
|
||||
if new_ids:
|
||||
# 用 ANY array 比較
|
||||
daily_rows = c.execute(text(f"""
|
||||
SELECT "日期", SUM(CAST("總業績" AS FLOAT)) AS rev
|
||||
FROM realtime_sales_monthly
|
||||
WHERE "商品ID" = ANY(:ids)
|
||||
AND CAST("日期" AS DATE) >= CURRENT_DATE - INTERVAL '{days_recent} days'
|
||||
GROUP BY "日期" ORDER BY "日期" ASC
|
||||
"""), {'ids': new_ids}).fetchall()
|
||||
else:
|
||||
daily_rows = []
|
||||
|
||||
return {
|
||||
'found': len(new_rows) > 0,
|
||||
'period': f"近 {days_recent} 天(vs 前 {days_baseline} 天 baseline)",
|
||||
'kpis': {
|
||||
'new_count': len(new_rows),
|
||||
'new_revenue': new_rev_total,
|
||||
'total_revenue': total_rev,
|
||||
'new_pct': new_rev_total / total_rev * 100 if total_rev else 0,
|
||||
'top1_revenue': float(new_rows[0][3]) if new_rows else 0,
|
||||
'days_recent': days_recent,
|
||||
},
|
||||
'new_products': [
|
||||
{'id': r[0], 'name': r[1], 'category': r[2] or '—',
|
||||
'revenue': float(r[3] or 0), 'qty': int(r[4] or 0),
|
||||
'orders': int(r[5] or 0), 'first_seen': str(r[6])}
|
||||
for r in new_rows
|
||||
],
|
||||
'sub_categories': [
|
||||
{'name': r[0], 'sku_count': int(r[1]),
|
||||
'revenue': float(r[2] or 0)} for r in sub_dist
|
||||
],
|
||||
'daily_total': [
|
||||
{'date': str(r[0]), 'revenue': float(r[1] or 0)}
|
||||
for r in daily_rows
|
||||
],
|
||||
}
|
||||
except Exception as e:
|
||||
sys_log.error(f"[query_new_products] {e}")
|
||||
return {'found': False, 'error': str(e)}
|
||||
|
||||
|
||||
def query_forecast_pre_event(event_name: str, event_date: str,
|
||||
before_days: int = 14, after_days: int = 7) -> dict:
|
||||
"""檔期前瞻:給定檔期日 + 名稱,回傳:
|
||||
|
||||
@@ -220,6 +220,7 @@ def _submenu_reports():
|
||||
('👥 客戶/訂單分析', 'cmd:ppt:customer')),
|
||||
_row(('🎯 檔期前瞻報告', 'await:forecast_event'),
|
||||
('🆚 多活動比較', 'await:promo_compare')),
|
||||
_row(('🆕 新品 30 天追蹤', 'cmd:ppt:new_product'),),
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ TEMPLATE_VERSIONS = {
|
||||
'customer': 'v3.1.0', # 2026-05-03 客戶/訂單分析(簡化 RFM,受資料層 user_id 限制)
|
||||
'forecast_pre_event': 'v3.1.0', # 2026-05-03 檔期前瞻報(baseline × lift_factor 預測 + 去年同檔期)
|
||||
'promo_compare': 'v3.1.0', # 2026-05-03 多活動 ROI 並排比較
|
||||
'new_product': 'v3.1.0', # 2026-05-03 新品 30 天追蹤(PM/採購)
|
||||
}
|
||||
|
||||
|
||||
@@ -2951,6 +2952,229 @@ def generate_vendor_ppt(yr, mo, db_data, ai_text: str) -> str:
|
||||
return path
|
||||
|
||||
|
||||
# ── 新品 30 天追蹤報告 ──────────────────────────────────────────────────
|
||||
def generate_new_product_ppt(db_data: dict, ai_text: str) -> str:
|
||||
"""新品 30 天追蹤報告 v3.1(PM/採購用)
|
||||
P1 封面:含新品數徽章 + 業績佔比
|
||||
P2 KPI 摘要 + 業績佔比評估
|
||||
P3 新品整體日業績曲線(爬榜軌跡)
|
||||
P4 新品依品類分佈(橫條 + 業績/SKU 數雙軸概念)
|
||||
P5-P7 新品 TOP 50 列表(自動分頁,含品類)
|
||||
P8 AI PM 戰術洞察(明星新品 / 該扶植 / 該下架)
|
||||
P9 附錄
|
||||
"""
|
||||
from pptx import Presentation
|
||||
from pptx.util import Cm
|
||||
|
||||
prs = Presentation()
|
||||
prs.slide_width = Cm(33.87)
|
||||
prs.slide_height = Cm(19.05)
|
||||
W = 33.87
|
||||
|
||||
period = db_data.get('period', '')
|
||||
kpis = db_data.get('kpis', {}) or {}
|
||||
new_prods = db_data.get('new_products', []) or []
|
||||
sub_cats = db_data.get('sub_categories', []) or []
|
||||
daily_total = db_data.get('daily_total', []) or []
|
||||
|
||||
new_count = int(kpis.get('new_count', 0))
|
||||
new_rev = float(kpis.get('new_revenue', 0))
|
||||
total_rev = float(kpis.get('total_revenue', 0))
|
||||
new_pct = float(kpis.get('new_pct', 0))
|
||||
|
||||
# 新品強度徽章
|
||||
if new_pct >= 8:
|
||||
strength_label, strength_color = '新品力強勁', '2A7A3F'
|
||||
elif new_pct >= 3:
|
||||
strength_label, strength_color = '新品力穩健', 'B88416'
|
||||
elif new_pct >= 1:
|
||||
strength_label, strength_color = '新品力偏弱', 'C96442'
|
||||
else:
|
||||
strength_label, strength_color = '新品力疲弱', 'B5342F'
|
||||
|
||||
# ── P1 封面 ──────────────────────────────────────────────
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
H = 19.05
|
||||
_add_rect(slide, 0, 0, W, H, _BG_PAPER)
|
||||
_add_rect(slide, 0, 0, 3.0, H, "2A7A3F")
|
||||
_add_rect(slide, 2.85, 0, 0.15, H, _BRAND_OG2)
|
||||
_add_rect(slide, W - 6.0, 0, 6.0, 0.45, _BRAND_OG2)
|
||||
_add_rect(slide, W - 6.0, 0.45, 6.0, 0.12, "2A7A3F")
|
||||
_add_rect(slide, 4.0, 8.4, 22.0, 0.06, "2A7A3F")
|
||||
|
||||
_add_rect(slide, 3.8, 1.4, 4.8, 0.85, _BRAND_OG2)
|
||||
_add_text(slide, "OPENCLAW", 3.8, 1.42, 4.8, 0.81,
|
||||
bold=True, size=12, color=_WHITE, align="center", valign="middle",
|
||||
latin_font=_FONT_LABEL)
|
||||
_add_text(slide, "NEW PRODUCT · 30-DAY TRACKING · AI INSIGHT",
|
||||
3.8, 2.45, 22, 0.55,
|
||||
bold=True, size=10, color=_BRAND_OG2,
|
||||
latin_font=_FONT_LABEL)
|
||||
_add_text(slide, f"新品追蹤報告\n{period}",
|
||||
3.8, 3.2, 25, 5.0,
|
||||
bold=True, size=42, color=_DARK_TEXT,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
_add_rect(slide, W - 9.0, 3.4, 5.0, 1.1, strength_color)
|
||||
_add_text(slide, f"新品力:{strength_label}",
|
||||
W - 9.0, 3.45, 5.0, 1.0,
|
||||
bold=True, size=14, color=_WHITE, align="center", valign="middle",
|
||||
ea_font=_FONT_BODY_EA)
|
||||
_add_text(slide,
|
||||
f"🆕 {new_count} 款新品 · 業績 NT${new_rev/10000:.1f}萬"
|
||||
f"(佔總業績 {new_pct:.1f}%)",
|
||||
3.8, 8.7, 27, 0.85,
|
||||
bold=True, size=14, color=_BRAND_OG2,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
|
||||
# 三個亮點
|
||||
if new_prods:
|
||||
top1 = new_prods[0]
|
||||
pitch_y = 10.2
|
||||
_add_rect(slide, 3.8, pitch_y, 0.45, 1.5, "2A7A3F")
|
||||
_add_text(slide, "🏆 最強新品",
|
||||
4.4, pitch_y + 0.1, 27, 0.55,
|
||||
bold=True, size=11, color="2A7A3F",
|
||||
ea_font=_FONT_BODY_EA, latin_font=_FONT_LABEL)
|
||||
_add_text(slide,
|
||||
f"{top1.get('name','')[:40]} "
|
||||
f"NT${float(top1.get('revenue',0)):,.0f}({top1.get('category','—')})",
|
||||
4.4, pitch_y + 0.7, 27, 0.75,
|
||||
size=12, color=_DARK_TEXT,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
|
||||
if sub_cats:
|
||||
top_cat = sub_cats[0]
|
||||
pitch_y2 = 12.1
|
||||
_add_rect(slide, 3.8, pitch_y2, 0.45, 1.5, "B88416")
|
||||
_add_text(slide, "📊 新品集中品類",
|
||||
4.4, pitch_y2 + 0.1, 27, 0.55,
|
||||
bold=True, size=11, color="B88416",
|
||||
ea_font=_FONT_BODY_EA, latin_font=_FONT_LABEL)
|
||||
_add_text(slide,
|
||||
f"{top_cat.get('name','')[:30]} "
|
||||
f"{top_cat.get('sku_count', 0)} 款新品 "
|
||||
f"業績 NT${top_cat.get('revenue', 0)/10000:.1f}萬",
|
||||
4.4, pitch_y2 + 0.7, 27, 0.75,
|
||||
size=12, color=_DARK_TEXT,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
|
||||
pitch_y3 = 14.0
|
||||
_add_rect(slide, 3.8, pitch_y3, 0.45, 1.5, "C96442")
|
||||
_add_text(slide, "🎯 業績佔比評估",
|
||||
4.4, pitch_y3 + 0.1, 27, 0.55,
|
||||
bold=True, size=11, color="C96442",
|
||||
ea_font=_FONT_BODY_EA, latin_font=_FONT_LABEL)
|
||||
_add_text(slide,
|
||||
f"新品 {new_pct:.1f}% — " + (
|
||||
"業界一流(>8%)" if new_pct >= 8 else
|
||||
"健康(3-8%)" if new_pct >= 3 else
|
||||
"需強化(<3%)"
|
||||
) + ";建議目標 5-10%(電商業界平均)",
|
||||
4.4, pitch_y3 + 0.7, 27, 0.75,
|
||||
size=12, color=_DARK_TEXT,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
|
||||
_add_text(slide, "Generated by OpenClaw AI Agent",
|
||||
W - 7.5, H - 1.4, 7.0, 0.5,
|
||||
size=9, color=_SUBTEXT, align="right", latin_font=_FONT_LABEL)
|
||||
_add_text(slide, f"📅 {datetime.now().strftime('%Y/%m/%d %H:%M')}",
|
||||
W - 7.5, H - 1.95, 7.0, 0.5,
|
||||
bold=True, size=11, color=_BRAND_OG2, align="right",
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
_add_footer(slide, W)
|
||||
|
||||
# ── P2 KPI ────────────────────────────────────────────
|
||||
s2 = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
_add_rect(s2, 0, 0, W, _SLIDE_H, _BG_PAPER)
|
||||
_add_header(s2, f"新品追蹤 KPI — {period}")
|
||||
avg_rev = new_rev / new_count if new_count else 0
|
||||
cards = [
|
||||
(_KPI_CARAMEL, "新品總數", f"{new_count} 款", None, "近 30 天進榜"),
|
||||
(_KPI_HONEY, "新品業績", f"NT${new_rev/10000:.1f}萬", None, "30 天累積"),
|
||||
(_KPI_MAHOGANY, "業績佔比", f"{new_pct:.1f}%", None, "vs 整體業績"),
|
||||
(_KPI_EARTH, "新品平均", f"NT${avg_rev/10000:.1f}萬", None, "單品 30 天均值"),
|
||||
]
|
||||
for i, (col, lbl, val, dp, dl) in enumerate(cards):
|
||||
_kpi_card_v2(s2, i * 7.8 + 0.5, 1.95, 7.4, 4.5,
|
||||
col, lbl, val, delta_pct=dp, delta_label=dl, sub=dl)
|
||||
|
||||
summary_text = (ai_text or '')[:400] if ai_text else "(暫無 AI 分析)"
|
||||
_add_rect(s2, 0.5, 7.0, W - 1.0, 0.7, "2A7A3F")
|
||||
_add_text(s2, "🆕 新品力解讀",
|
||||
1.1, 7.05, W - 1.5, 0.6, bold=True, size=13, color=_WHITE,
|
||||
valign="middle", ea_font=_FONT_BODY_EA)
|
||||
_add_rect(s2, 0.5, 7.7, W - 1.0, 6.4, _WHITE, line_hex=_SUBTLE)
|
||||
_add_rect(s2, 0.5, 7.7, 0.4, 6.4, "2A7A3F")
|
||||
_add_text(s2, summary_text,
|
||||
1.2, 7.95, W - 2.0, 5.9,
|
||||
size=13, color=_DARK_TEXT, wrap=True,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
_add_footer(s2, W)
|
||||
|
||||
# ── P3 新品整體日業績曲線 ───────────────────────────────
|
||||
if daily_total:
|
||||
s3 = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
_add_rect(s3, 0, 0, W, _SLIDE_H, _BG_PAPER)
|
||||
_add_header(s3, "新品整體日業績走勢(爬榜軌跡)")
|
||||
d_dates = [d.get('date', '') for d in daily_total]
|
||||
d_revs = [float(d.get('revenue', 0)) for d in daily_total]
|
||||
chart_w = W - 0.8
|
||||
chart_h = 12.5
|
||||
buf = _mpl_line_chart_png(
|
||||
d_dates, d_revs, prev_vals=None,
|
||||
total_width_cm=chart_w, total_height_cm=chart_h,
|
||||
title="新品 30 天日業績走勢",
|
||||
curr_label="新品合計"
|
||||
)
|
||||
if buf:
|
||||
_add_image_from_buf(s3, buf, 0.4, 1.95, chart_w, chart_h)
|
||||
_add_footer(s3, W)
|
||||
|
||||
# ── P4 新品依品類分佈 ────────────────────────────────
|
||||
if sub_cats:
|
||||
s4 = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
_add_rect(s4, 0, 0, W, _SLIDE_H, _BG_PAPER)
|
||||
_add_header(s4, "新品依品類分佈")
|
||||
names = [c.get('name', '')[:14] for c in sub_cats[:8]]
|
||||
revs = [float(c.get('revenue', 0)) for c in sub_cats[:8]]
|
||||
chart_w = W - 0.8
|
||||
chart_h = 11.5
|
||||
buf = _mpl_horiz_bar_png(names, revs,
|
||||
total_width_cm=chart_w,
|
||||
total_height_cm=chart_h,
|
||||
value_unit="萬",
|
||||
title="新品業績排行(依品類)",
|
||||
highlight_top_n=3)
|
||||
if buf:
|
||||
_add_image_from_buf(s4, buf, 0.4, 1.95, chart_w, chart_h)
|
||||
|
||||
# 底部結論
|
||||
_add_rect(s4, 0.4, 14.0, W - 0.8, 1.4, _BRAND_OG2)
|
||||
cat_summary = ' · '.join(
|
||||
f"{c.get('name','')[:8]} {c.get('sku_count',0)} 款"
|
||||
for c in sub_cats[:5]
|
||||
)
|
||||
_add_text(s4, f"📊 品類新品數:{cat_summary}",
|
||||
0.7, 14.15, W - 1.4, 1.1,
|
||||
bold=True, size=12, color=_WHITE, valign="middle", wrap=True,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
_add_footer(s4, W)
|
||||
|
||||
# ── P5-P7 新品 TOP 50 ──────────────────────────────────
|
||||
_product_table_slide(prs, f"新品 TOP {min(50, len(new_prods))} — {period}",
|
||||
new_prods, max_items=50)
|
||||
|
||||
# ── P8 AI 洞察 ─────────────────────────────────────────
|
||||
_ai_insight_slide(prs, ai_text)
|
||||
|
||||
# ── P9 附錄 ────────────────────────────────────────────
|
||||
_appendix_slide(prs, 'new_product', period)
|
||||
|
||||
path = _new_path("new_product")
|
||||
prs.save(path)
|
||||
return path
|
||||
|
||||
|
||||
# ── 多活動 ROI 橫向比較報告 ─────────────────────────────────────────────
|
||||
def generate_promo_compare_ppt(label: str, db_data: dict, ai_text: str) -> str:
|
||||
"""多活動 ROI 比較報告:2-N 個促銷活動並排比較
|
||||
|
||||
Reference in New Issue
Block a user