feat(ppt): forecast_pre_event report v3.1.0 (BU pre-event battle plan)
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
Wave 2.1:檔期前瞻報告 — 給 BU 主管在檔期前 14 天決定備戰策略。 generate_forecast_pre_event_ppt — 7 頁 - P1 封面:含倒數天數徽章(備戰期/衝刺期/檔期中/已結束)+ 三段預期業績 - P2 KPI 三段對比:本期預期 / 去年同檔期 / Baseline 日均 / 已執行業績 - P3 去年同檔期業績曲線(matplotlib 折線,預測基準視覺化) - P4 庫存盤點 TOP 30(基於 baseline 期銷量) - P5 AI 戰術洞察 - P6 附錄 query_forecast_pre_event(event_name, event_date) - baseline 期:檔期前 60-30 天(去除檔期前期效應) - 去年同檔期:去年同日期 ± 7 天 - 本期準備窗口:檔期前 14 天到檔期後 7 天 - 預期業績 = baseline 日均 × 21 天 × lift_factor - lift_factor 靜態知識(母親節 1.40 / 618 1.45 / 雙11 1.65 / 黑五 1.45 等) _ppt_ai_analysis 加 is_forecast 分支 - 角色:BU 主管 + 行銷投放主管 + 採購三合一 - 結構:檔期定位 / 準備窗口進度 / 庫存戰術 / 廣告滿額門檻 / SMART 三層(檔期前7/當日+3/檔期後7)/ 風險預警 - max_tokens 2000 路由: - /ppt forecast 母親節 2026/05/12 - /ppt forecast 618 2026/06/18 Telegram 按鈕:「🎯 檔期前瞻報告」(await:forecast_event) bump TEMPLATE_VERSIONS['forecast_pre_event'] = v3.1.0 煙霧測試:7 頁 140KB 全綠。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -218,6 +218,7 @@ def _submenu_reports():
|
||||
('📊 TTM 滾動 12 月', 'cmd:ppt:ttm')),
|
||||
_row(('🗂 品類深度報告', 'await:category_deep'),
|
||||
('👥 客戶/訂單分析', 'cmd:ppt:customer')),
|
||||
_row(('🎯 檔期前瞻報告', 'await:forecast_event'),),
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ TEMPLATE_VERSIONS = {
|
||||
'ttm': 'v3.1.0', # 2026-05-03 TTM 滾動 12 月
|
||||
'category': 'v3.1.0', # 2026-05-03 品類深度報告(90 天縱向 + 子品類 + 新進榜)
|
||||
'customer': 'v3.1.0', # 2026-05-03 客戶/訂單分析(簡化 RFM,受資料層 user_id 限制)
|
||||
'forecast_pre_event': 'v3.1.0', # 2026-05-03 檔期前瞻報(baseline × lift_factor 預測 + 去年同檔期)
|
||||
'bcg': 'v2.0', # DEPRECATED — 從未落地
|
||||
}
|
||||
|
||||
@@ -2949,6 +2950,235 @@ def generate_vendor_ppt(yr, mo, db_data, ai_text: str) -> str:
|
||||
return path
|
||||
|
||||
|
||||
# ── 檔期前瞻報告 ───────────────────────────────────────────────────────
|
||||
def generate_forecast_pre_event_ppt(event_name: str, event_date: str,
|
||||
db_data: dict, ai_text: str) -> str:
|
||||
"""檔期前瞻報告 v3.1:給 BU 主管在檔期前 14 天決定備戰策略
|
||||
P1 封面(含倒數天數徽章 + 預期業績 vs baseline)
|
||||
P2 KPI 三段對比:去年同檔期 / baseline / 本期準備窗口
|
||||
P3 去年同檔期業績曲線 + 本期 baseline 對比
|
||||
P4 庫存盤點:TOP 30 商品(基於 baseline 期)
|
||||
P5 AI 戰術洞察(檔期戰術、廣告投放、庫存補貨、競品阻擊)
|
||||
P6 附錄
|
||||
"""
|
||||
from pptx import Presentation
|
||||
from pptx.util import Cm
|
||||
from datetime import datetime as _dt
|
||||
|
||||
prs = Presentation()
|
||||
prs.slide_width = Cm(33.87)
|
||||
prs.slide_height = Cm(19.05)
|
||||
W = 33.87
|
||||
|
||||
baseline = db_data.get('baseline', {}) or {}
|
||||
last_year = db_data.get('last_year', {}) or {}
|
||||
prep_window = db_data.get('prep_window', {}) or {}
|
||||
top_prods = db_data.get('top_products', []) or []
|
||||
forecast = db_data.get('forecast', {}) or {}
|
||||
window_start = db_data.get('window_start', '')
|
||||
window_end = db_data.get('window_end', '')
|
||||
|
||||
expected_rev = float(forecast.get('expected_revenue', 0))
|
||||
lift = float(forecast.get('lift_factor', 1.0))
|
||||
b_daily = float(baseline.get('avg_daily_revenue', 0))
|
||||
ly_rev = float(last_year.get('revenue', 0))
|
||||
|
||||
# 倒數天數
|
||||
try:
|
||||
ev_d = _dt.strptime(event_date.replace('/', '-'), '%Y-%m-%d').date()
|
||||
days_to_event = (ev_d - _dt.now().date()).days
|
||||
except Exception:
|
||||
days_to_event = 0
|
||||
|
||||
# 定位徽章
|
||||
if days_to_event > 7:
|
||||
urgency_label, urgency_color = '備戰期', '2A7A3F'
|
||||
elif days_to_event > 0:
|
||||
urgency_label, urgency_color = '衝刺期', 'B88416'
|
||||
elif days_to_event >= -7:
|
||||
urgency_label, urgency_color = '檔期中', 'C96442'
|
||||
else:
|
||||
urgency_label, urgency_color = '已結束', '9B9081'
|
||||
|
||||
# ── 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, urgency_color)
|
||||
_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, urgency_color)
|
||||
_add_rect(slide, 4.0, 8.4, 22.0, 0.06, urgency_color)
|
||||
|
||||
_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, "PRE-EVENT FORECAST · AI BATTLE PLAN",
|
||||
3.8, 2.45, 22, 0.55,
|
||||
bold=True, size=10, color=_BRAND_OG2,
|
||||
latin_font=_FONT_LABEL)
|
||||
_add_text(slide, f"檔期前瞻報告\n{event_name}",
|
||||
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, urgency_color)
|
||||
if days_to_event > 0:
|
||||
urgency_text = f"還有 {days_to_event} 天 · {urgency_label}"
|
||||
elif days_to_event >= -7:
|
||||
urgency_text = f"檔期中 · 第 {abs(days_to_event)+1} 天"
|
||||
else:
|
||||
urgency_text = f"檔期已過 {abs(days_to_event)} 天"
|
||||
_add_text(slide, urgency_text,
|
||||
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"檔期日 {event_date} · 準備窗口 {window_start} ~ {window_end}",
|
||||
3.8, 8.7, 27, 0.85,
|
||||
bold=True, size=14, color=_BRAND_OG2,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
|
||||
# 預期 vs baseline
|
||||
pitch_y = 10.2
|
||||
_add_rect(slide, 3.8, pitch_y, 0.45, 1.5, "C96442")
|
||||
_add_text(slide, "🎯 本期預期業績", 4.4, pitch_y + 0.1, 27, 0.55,
|
||||
bold=True, size=11, color="C96442",
|
||||
ea_font=_FONT_BODY_EA, latin_font=_FONT_LABEL)
|
||||
_add_text(slide,
|
||||
f"NT${expected_rev/10000:.1f}萬"
|
||||
+ (f"(baseline NT${b_daily/10000:.1f}萬/日 × {(window_end and 21) or 21} 天 × {lift:.2f} 倍)"
|
||||
if b_daily else ''),
|
||||
4.4, pitch_y + 0.7, 27, 0.75,
|
||||
size=12, color=_DARK_TEXT,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
|
||||
pitch_y2 = pitch_y + 1.9
|
||||
_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)
|
||||
if ly_rev > 0:
|
||||
ly_compare_pct = (expected_rev - ly_rev) / ly_rev * 100 if ly_rev else 0
|
||||
ly_arrow = "▲" if ly_compare_pct > 0 else "▼"
|
||||
_add_text(slide,
|
||||
f"去年同期 NT${ly_rev/10000:.1f}萬 → 本期預期 {ly_arrow} {abs(ly_compare_pct):.1f}%",
|
||||
4.4, pitch_y2 + 0.7, 27, 0.75,
|
||||
size=12, color=_DARK_TEXT,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
else:
|
||||
_add_text(slide, "去年同檔期無資料 — 預測信心度較低",
|
||||
4.4, pitch_y2 + 0.7, 27, 0.75,
|
||||
size=12, color=_SUBTEXT,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
|
||||
# 已執行 vs 目標
|
||||
prep_rev = float(prep_window.get('revenue', 0))
|
||||
pitch_y3 = pitch_y2 + 1.9
|
||||
_add_rect(slide, 3.8, pitch_y3, 0.45, 1.5, "2A7A3F")
|
||||
_add_text(slide, "✅ 本期準備窗口已執行",
|
||||
4.4, pitch_y3 + 0.1, 27, 0.55,
|
||||
bold=True, size=11, color="2A7A3F",
|
||||
ea_font=_FONT_BODY_EA, latin_font=_FONT_LABEL)
|
||||
_add_text(slide,
|
||||
f"已過 {prep_window.get('days_passed', 0)} / {prep_window.get('days_total', 0)} 天"
|
||||
f" · 累積業績 NT${prep_rev/10000:.1f}萬"
|
||||
+ (f"(達成預期 {prep_rev/expected_rev*100:.1f}%)" if expected_rev else ''),
|
||||
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"檔期業績三段對比 — {event_name}")
|
||||
cards = [
|
||||
(_KPI_CARAMEL, "本期預期業績", f"NT${expected_rev/10000:.1f}萬",
|
||||
f"baseline × {lift:.2f}"),
|
||||
(_KPI_HONEY, "去年同檔期", f"NT${ly_rev/10000:.1f}萬",
|
||||
last_year.get('period', '—')[:25]),
|
||||
(_KPI_MAHOGANY, "Baseline 日均", f"NT${b_daily/10000:.1f}萬",
|
||||
f"({baseline.get('days', 0)} 天均值)"),
|
||||
(_KPI_EARTH, "已執行業績", f"NT${prep_rev/10000:.1f}萬",
|
||||
f"{prep_window.get('days_passed', 0)}/{prep_window.get('days_total', 0)} 天"),
|
||||
]
|
||||
for i, (col, lbl, val, sub) in enumerate(cards):
|
||||
_kpi_card_v2(s2, i * 7.8 + 0.5, 1.95, 7.4, 4.5,
|
||||
col, lbl, val, delta_pct=None, delta_label=sub, sub=sub)
|
||||
|
||||
# AI 解讀
|
||||
summary_text = (ai_text or '')[:400] if ai_text else "(暫無 AI 分析)"
|
||||
_add_rect(s2, 0.5, 7.0, W - 1.0, 0.7, urgency_color)
|
||||
_add_text(s2, f"🎯 {event_name} 戰術解讀",
|
||||
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, urgency_color)
|
||||
_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: 去年同檔期業績曲線 ────────────────────────────────
|
||||
ly_daily = last_year.get('daily', [])
|
||||
if ly_daily:
|
||||
s3 = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
_add_rect(s3, 0, 0, W, _SLIDE_H, _BG_PAPER)
|
||||
_add_header(s3, f"去年同檔期業績曲線 — 預測基準")
|
||||
d_dates = [d.get('date', '') for d in ly_daily]
|
||||
d_revs = [float(d.get('revenue', 0)) for d in ly_daily]
|
||||
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=f"去年同檔期 {last_year.get('period', '')} 日業績曲線",
|
||||
curr_label="去年同期"
|
||||
)
|
||||
if buf:
|
||||
_add_image_from_buf(s3, buf, 0.4, 1.95, chart_w, chart_h)
|
||||
# 結論帶
|
||||
if d_revs:
|
||||
avg_d = sum(d_revs) / len(d_revs)
|
||||
max_d = max(d_revs)
|
||||
_add_rect(s3, 0.4, 14.7, W - 0.8, 1.0, _BRAND_OG2)
|
||||
_add_text(s3,
|
||||
f"📊 去年同檔期日均 NT${avg_d/10000:.1f}萬 · "
|
||||
f"最高單日 NT${max_d/10000:.1f}萬 · "
|
||||
f"本期預期 baseline × {lift:.2f} 倍",
|
||||
0.7, 14.85, W - 1.4, 0.7,
|
||||
bold=True, size=12, color=_WHITE, valign="middle",
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
_add_footer(s3, W)
|
||||
|
||||
# ── P4: 庫存盤點(baseline 期 TOP 30)─────────────────────
|
||||
if top_prods:
|
||||
_product_table_slide(prs,
|
||||
f"庫存盤點對象 TOP {min(30, len(top_prods))} — 基於 baseline 期銷量",
|
||||
top_prods, max_items=30)
|
||||
|
||||
# ── P5: AI 戰術洞察 ─────────────────────────────────────
|
||||
_ai_insight_slide(prs, ai_text)
|
||||
|
||||
# ── P6: 附錄 ─────────────────────────────────────────────
|
||||
_appendix_slide(prs, 'forecast_pre_event', f"{event_name} ({event_date})")
|
||||
|
||||
path = _new_path("forecast_pre_event")
|
||||
prs.save(path)
|
||||
return path
|
||||
|
||||
|
||||
# ── 客戶/訂單分析報告(簡化版 RFM)────────────────────────────────────────
|
||||
def generate_customer_analytics_ppt(period_label: str, db_data: dict, ai_text: str) -> str:
|
||||
"""客戶/訂單分析報告 v3.1(行銷主管用)
|
||||
|
||||
Reference in New Issue
Block a user