feat(ppt): period_review unified generator for quarterly/half/annual/ttm
All checks were successful
CD Pipeline / deploy (push) Successful in 3m37s
All checks were successful
CD Pipeline / deploy (push) Successful in 3m37s
Wave 1.2:用一份 generator 解 4 種時間維度報表(季/半年/年/TTM)。 generate_period_review_ppt(services/ppt_generator.py)— 12 頁 - 期間類型徽章自動切換(季報/半年報/年報/TTM 滾動) - P1 封面:含業績狀態徽章 + elevator pitch + 期間動能 - P2 執行摘要:KPI v2 含 △% (vs 上季/上半/去年) + AI 解讀 + YoY 對比帶 - P3 月度業績走勢(matplotlib 折線:本期 + 上期 + 月均線 + 高低點)+ 4 卡指標 - P4 品類分析:橫條 + 帕雷托雙視圖 - P5-P7 TOP 50 商品(自動分頁) - P8 TOP 30 廠商 - P9 MCP 市場情報 - P10 AI 結構化洞察 - P11 附錄 query_period_summary(routes/openclaw_bot_routes.py) - 一次拉齊:kpis / monthly_breakdown / top_products(50) / top_categories(8) / top_vendors(30) - 自動算月度聚合(PostgreSQL TO_CHAR) 路由層加 4 種 sub_type 分支: - /ppt quarterly [YYYY/Q1-4] 當季或指定季 - /ppt half_yearly [YYYY/H1-2] 當半年或指定半年 - /ppt annual [YYYY] 當年或指定年 - /ppt ttm 最近 12 個月(自動算到本月底) 每種自動抓三段資料:本期 + 上期同等 + 去年同期 - quarterly: 上季 + 去年同季 - half_yearly: 上半年 + 去年同半年 - annual: 去年 + 前年 - ttm: 上一個 TTM (24-12 月前) + 同上 _ppt_ai_analysis 加 is_period 分支 - 角色:策略顧問 + BU 主管 + CFO 三合一視角 - 結構:整體解讀 / 市場趨勢對位 / 月度走勢分析 / 品類結構 / 戰略級 SMART / 風險預警 - SMART 行動分三層:下期立即啟動(30 天)/ 下期戰略(60-90 天)/ 下下期預備(6-12 月) - max_tokens 2600,字數 1000-1300 Telegram 按鈕:報表選單加 4 顆季/半/年/TTM 按鈕 bump TEMPLATE_VERSIONS:quarterly / half_yearly / annual / ttm 全 v3.1.0 煙霧測試:4 種全綠,每份 12 頁 220KB。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,10 @@ TEMPLATE_VERSIONS = {
|
||||
# 但路由層未綁定指令;保留版本字串避免如未來重啟時快取 schema 對不上。
|
||||
'growth': 'v2.0', # DEPRECATED — 從未落地
|
||||
'vendor': 'v3.1.0', # 2026-05-03 喚醒 + v3 暖紙風 + matplotlib 雙視圖 + 採購策略 SMART prompt + 集中度警示
|
||||
'quarterly': 'v3.1.0', # 2026-05-03 季報(period_review 共用 generator)
|
||||
'half_yearly': 'v3.1.0', # 2026-05-03 半年報
|
||||
'annual': 'v3.1.0', # 2026-05-03 年報
|
||||
'ttm': 'v3.1.0', # 2026-05-03 TTM 滾動 12 月
|
||||
'bcg': 'v2.0', # DEPRECATED — 從未落地
|
||||
}
|
||||
|
||||
@@ -2943,6 +2947,348 @@ def generate_vendor_ppt(yr, mo, db_data, ai_text: str) -> str:
|
||||
return path
|
||||
|
||||
|
||||
# ── 期間回顧報告(quarterly / half_yearly / annual / ttm 共用)───────────────
|
||||
def generate_period_review_ppt(period_type: str, period_label: str,
|
||||
db_data: dict, ai_text: str) -> str:
|
||||
"""期間回顧報告 — 一份 generator 解 4 種:
|
||||
period_type: 'quarterly' → '2026 Q1'
|
||||
'half_yearly' → '2026 H1'
|
||||
'annual' → '2026'
|
||||
'ttm' → 'TTM 2025-05~2026-04'
|
||||
db_data: {
|
||||
kpis, monthly_breakdown, top_products, top_categories, top_vendors,
|
||||
prev_period: dict (上一期同等)
|
||||
yoy_period: dict (去年同期)
|
||||
mcp: str
|
||||
}
|
||||
"""
|
||||
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
|
||||
|
||||
kpis = db_data.get('kpis', {}) or {}
|
||||
monthly = db_data.get('monthly_breakdown', []) or []
|
||||
top_cats = db_data.get('top_categories', []) or []
|
||||
top_prods = db_data.get('top_products', []) or []
|
||||
top_vendors = db_data.get('top_vendors', []) or []
|
||||
prev_period = db_data.get('prev_period') or {}
|
||||
yoy_period = db_data.get('yoy_period') or {}
|
||||
mcp_text = db_data.get('mcp', '') or ''
|
||||
|
||||
rev = float(kpis.get('revenue', 0))
|
||||
ord_ = int(kpis.get('orders', 0))
|
||||
gm = float(kpis.get('gross_margin', 0))
|
||||
aov = float(kpis.get('avg_order', rev / ord_ if ord_ else 0))
|
||||
|
||||
# 期間類型徽章
|
||||
type_badges = {
|
||||
'quarterly': ('季報', 'QUARTERLY REVIEW', _KPI_HONEY),
|
||||
'half_yearly': ('半年報', 'HALF-YEARLY REVIEW', _KPI_MAHOGANY),
|
||||
'annual': ('年報', 'ANNUAL REVIEW', _BRAND_OG2),
|
||||
'ttm': ('TTM 滾動', 'TRAILING 12 MONTHS', _KPI_EARTH),
|
||||
}
|
||||
type_label, type_en, type_color = type_badges.get(period_type, ('期間回顧', 'PERIOD REVIEW', _BRAND_OG))
|
||||
|
||||
# ── P1: 封面 ─────────────────────────────────────────────
|
||||
elevator = _compute_elevator_pitch(
|
||||
{'revenue': rev, 'orders': ord_, 'gross_margin': gm, 'top_categories': top_cats},
|
||||
prev_period.get('kpis') if prev_period else None
|
||||
)
|
||||
_period_review_cover_slide(prs, period_label, type_label, type_en, type_color,
|
||||
rev, ord_, gm, aov, elevator)
|
||||
|
||||
# ── P2: 執行摘要(KPI v2 + QoQ/HoH/YoY 對比帶)──────────────
|
||||
s2 = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
_add_rect(s2, 0, 0, W, _SLIDE_H, _BG_PAPER)
|
||||
_add_header(s2, f"{type_label}執行摘要 — {period_label}")
|
||||
|
||||
def _delta(curr_v, prev_dict, key, is_pp=False):
|
||||
if not prev_dict:
|
||||
return None
|
||||
prev_kpis = prev_dict.get('kpis', {})
|
||||
prev_v = float(prev_kpis.get(key, 0) or 0)
|
||||
if not prev_v:
|
||||
return None
|
||||
if is_pp:
|
||||
return float(curr_v) - prev_v
|
||||
return (float(curr_v) - prev_v) / prev_v * 100
|
||||
|
||||
d_rev = _delta(rev, prev_period, 'revenue')
|
||||
d_ord = _delta(ord_, prev_period, 'orders')
|
||||
d_gm = _delta(gm, prev_period, 'gross_margin', is_pp=True)
|
||||
d_aov = _delta(aov, prev_period, 'avg_order')
|
||||
|
||||
qoh_label = {
|
||||
'quarterly': 'vs 上季',
|
||||
'half_yearly': 'vs 上半',
|
||||
'annual': 'vs 去年',
|
||||
'ttm': 'vs 上期',
|
||||
}.get(period_type, 'vs 上期')
|
||||
|
||||
kpis_v2 = [
|
||||
(_KPI_CARAMEL, "期間業績", f"NT${rev/10000:.1f}萬", d_rev, qoh_label),
|
||||
(_KPI_HONEY, "期間訂單", f"{ord_:,} 筆", d_ord, qoh_label),
|
||||
(_KPI_MAHOGANY, "毛利率", f"{gm:.1f}%", d_gm, f"{qoh_label}(pp)"),
|
||||
(_KPI_EARTH, "平均客單", f"NT${aov:,.0f}", d_aov, qoh_label),
|
||||
]
|
||||
for i, (col, lbl, val, dp, dl) in enumerate(kpis_v2):
|
||||
_kpi_card_v2(s2, i * 7.8 + 0.5, 1.95, 7.4, 4.5,
|
||||
col, lbl, val, delta_pct=dp, delta_label=dl)
|
||||
|
||||
# 高階解讀區塊
|
||||
_add_rect(s2, 0.5, 7.0, W - 1.0, 0.7, type_color)
|
||||
_add_text(s2, f"📊 {type_label}高階營運解讀(AI Generated)",
|
||||
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, 5.8, _WHITE, line_hex=_SUBTLE)
|
||||
_add_rect(s2, 0.5, 7.7, 0.4, 5.8, type_color)
|
||||
|
||||
# 抽取 AI 整體解讀段
|
||||
summary_text = ""
|
||||
capture = False
|
||||
for line in (ai_text or '').split('\n'):
|
||||
if any(k in line for k in ['整體業績解讀', '高階營運', '整體表現']):
|
||||
capture = True
|
||||
continue
|
||||
if capture:
|
||||
if line.strip().startswith('【') and '整體' not in line:
|
||||
break
|
||||
if line.strip():
|
||||
summary_text += line + "\n"
|
||||
if len(summary_text) > 350: break
|
||||
if not summary_text.strip():
|
||||
for line in (ai_text or '').split('\n'):
|
||||
if line.strip() and not line.startswith('【'):
|
||||
summary_text += line + "\n"
|
||||
if len(summary_text) > 350: break
|
||||
if not summary_text.strip():
|
||||
summary_text = (ai_text or '')[:350] + "…" if ai_text else "(暫無 AI 分析)"
|
||||
|
||||
_add_text(s2, summary_text.strip(),
|
||||
1.2, 7.95, W - 2.0, 5.3,
|
||||
size=13, color=_DARK_TEXT, wrap=True,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
|
||||
# YoY 對比帶(如有)
|
||||
yoy_y = 13.8
|
||||
if yoy_period and yoy_period.get('kpis'):
|
||||
prev_yr_rev = float(yoy_period['kpis'].get('revenue', 0) or 0)
|
||||
if prev_yr_rev:
|
||||
yoy = (rev - prev_yr_rev) / prev_yr_rev * 100
|
||||
yoy_color = "2A7A3F" if yoy > 0 else "B5342F"
|
||||
arrow = "▲" if yoy > 0 else "▼"
|
||||
_add_rect(s2, 0.5, yoy_y, W - 1.0, 1.4, yoy_color)
|
||||
_add_text(s2,
|
||||
f"📅 YoY 同期對比:去年 {yoy_period.get('period_label', '同期')} 業績 NT${prev_yr_rev/10000:.1f}萬"
|
||||
f" → 本期 {arrow} {abs(yoy):.1f}%",
|
||||
0.7, yoy_y + 0.1, W - 1.4, 1.2,
|
||||
bold=True, size=13, color=_WHITE, valign="middle",
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
_add_footer(s2, W)
|
||||
|
||||
# ── P3: 月度業績走勢(折線圖:本期 + 上期 + 去年同期) ────────
|
||||
s3 = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
_add_rect(s3, 0, 0, W, _SLIDE_H, _BG_PAPER)
|
||||
_add_header(s3, f"{type_label}業績走勢 — {period_label}(月度)")
|
||||
if monthly:
|
||||
m_dates = [m.get('month', '') for m in monthly]
|
||||
m_revs = [float(m.get('revenue', 0)) for m in monthly]
|
||||
prev_revs = None
|
||||
if prev_period and prev_period.get('monthly_breakdown'):
|
||||
prev_revs = [float(m.get('revenue', 0))
|
||||
for m in prev_period['monthly_breakdown']]
|
||||
chart_w = W - 0.8
|
||||
chart_h = 11.0
|
||||
buf = _mpl_line_chart_png(
|
||||
m_dates, m_revs, prev_vals=prev_revs,
|
||||
total_width_cm=chart_w, total_height_cm=chart_h,
|
||||
title=f"月度業績走勢({type_label} {period_label})",
|
||||
curr_label="本期", prev_label="上期"
|
||||
)
|
||||
if buf:
|
||||
_add_image_from_buf(s3, buf, 0.4, 1.95, chart_w, chart_h)
|
||||
|
||||
# 底部 4 卡:合計 / 月均 / 最高月 / 最低月
|
||||
if m_revs:
|
||||
avg_m = sum(m_revs) / len(m_revs)
|
||||
max_m, min_m = max(m_revs), min(m_revs)
|
||||
ins_y = 13.3
|
||||
ins_h = 2.4
|
||||
card_w = (W - 1.0 - 0.3 * 3) / 4
|
||||
cards = [
|
||||
(_BRAND_OG, "📊 期間合計", f"NT${sum(m_revs)/10000:.1f}萬", f"{len(m_revs)} 個月"),
|
||||
(_KPI_HONEY, "📈 月均業績", f"NT${avg_m/10000:.1f}萬", "月平均"),
|
||||
(_KPI_MAHOGANY, "🏆 最高月", f"NT${max_m/10000:.1f}萬",
|
||||
m_dates[m_revs.index(max_m)] if m_revs else "—"),
|
||||
(_KPI_EARTH, "📉 最低月", f"NT${min_m/10000:.1f}萬",
|
||||
m_dates[m_revs.index(min_m)] if m_revs else "—"),
|
||||
]
|
||||
for i, (col, lbl, val, sub) in enumerate(cards):
|
||||
cx = 0.5 + i * (card_w + 0.3)
|
||||
_add_rect(s3, cx, ins_y, card_w, ins_h, col)
|
||||
_add_rect(s3, cx, ins_y, 0.15, ins_h, "FFFFFF")
|
||||
_add_text(s3, lbl, cx + 0.3, ins_y + 0.2, card_w - 0.5, 0.5,
|
||||
size=10, color="FAF7F0",
|
||||
ea_font=_FONT_BODY_EA, latin_font=_FONT_LABEL)
|
||||
_add_text(s3, val, cx + 0.2, ins_y + 0.75, card_w - 0.4, 0.95,
|
||||
bold=True, size=18, color="FFFFFF", align="center", valign="middle",
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
_add_text(s3, sub, cx + 0.2, ins_y + 1.75, card_w - 0.4, 0.55,
|
||||
size=9, color="FAF7F0", align="center",
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
else:
|
||||
_add_empty_state(s3, "無月度業績資料",
|
||||
"請確認該期間是否已有銷售資料。", W)
|
||||
_add_footer(s3, W)
|
||||
|
||||
# ── P4: 品類分析(橫條 + 帕雷托)────────────────────────────
|
||||
if top_cats:
|
||||
s4 = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
_add_rect(s4, 0, 0, W, _SLIDE_H, _BG_PAPER)
|
||||
_add_header(s4, f"{type_label}品類業績結構分析 — {period_label}")
|
||||
cats_disp = [c.get('cat', '')[:14] for c in top_cats[:8]]
|
||||
revs_cats = [float(c.get('revenue', 0)) for c in top_cats[:8]]
|
||||
chart_w_left = W * 0.5 - 0.4
|
||||
chart_h = 12.5
|
||||
buf1 = _mpl_horiz_bar_png(cats_disp, revs_cats,
|
||||
total_width_cm=chart_w_left,
|
||||
total_height_cm=chart_h,
|
||||
value_unit="萬",
|
||||
title="① 業績排行(焦糖橘=TOP3)",
|
||||
highlight_top_n=3)
|
||||
if buf1:
|
||||
_add_image_from_buf(s4, buf1, 0.4, 1.95, chart_w_left, chart_h)
|
||||
chart_w_right = W * 0.5 - 0.4
|
||||
rx = W * 0.5 + 0.0
|
||||
buf2 = _mpl_pareto_chart_png(cats_disp, revs_cats,
|
||||
total_width_cm=chart_w_right,
|
||||
total_height_cm=chart_h,
|
||||
title="② 帕雷托累計貢獻(80% 主力線)")
|
||||
if buf2:
|
||||
_add_image_from_buf(s4, buf2, rx, 1.95, chart_w_right, chart_h)
|
||||
_add_footer(s4, W)
|
||||
|
||||
# ── P5-P7: TOP 50 商品(自動分頁)──────────────────────────
|
||||
_product_table_slide(prs, f"{type_label}熱銷商品 TOP 50 — {period_label}",
|
||||
top_prods, max_items=50)
|
||||
|
||||
# ── P8: TOP 30 廠商 ────────────────────────────────────────
|
||||
if top_vendors:
|
||||
_vendor_table_slide(prs, top_vendors[:30], period_label, {},
|
||||
sum(float(v.get('sales', 0)) for v in top_vendors), max_items=30)
|
||||
|
||||
# ── P9: MCP 市場情報 ──────────────────────────────────────
|
||||
_mcp_intel_slide(prs, mcp_text)
|
||||
|
||||
# ── P10: AI 結構化洞察 ─────────────────────────────────────
|
||||
_ai_insight_slide(prs, ai_text)
|
||||
|
||||
# ── P11: 附錄 ─────────────────────────────────────────────
|
||||
_appendix_slide(prs, period_type, period_label)
|
||||
|
||||
path = _new_path(period_type)
|
||||
prs.save(path)
|
||||
return path
|
||||
|
||||
|
||||
def _period_review_cover_slide(prs, period_label, type_label, type_en, type_color,
|
||||
rev, ord_, gm, aov, elevator):
|
||||
"""期間回顧封面 — 含期間類型徽章"""
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
W = 33.87
|
||||
H = 19.05
|
||||
|
||||
_add_rect(slide, 0, 0, W, H, _BG_PAPER)
|
||||
_add_rect(slide, 0, 0, 3.0, H, type_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, type_color)
|
||||
_add_rect(slide, 4.0, 8.4, 22.0, 0.06, type_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, f"{type_en} · AI INSIGHT",
|
||||
3.8, 2.45, 22, 0.55,
|
||||
bold=True, size=10, color=_BRAND_OG2,
|
||||
latin_font=_FONT_LABEL)
|
||||
|
||||
_add_text(slide, f"{type_label}\n{period_label}",
|
||||
3.8, 3.2, 25, 5.0,
|
||||
bold=True, size=44, color=_DARK_TEXT,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
|
||||
status_color = elevator.get('status_color', _SUBTEXT)
|
||||
_add_rect(slide, W - 9.0, 3.4, 5.0, 1.1, status_color)
|
||||
_add_text(slide, f"業績狀態:{elevator.get('status', '—')}",
|
||||
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"業績 NT${rev:,.0f}({rev/10000:.1f}萬) · 訂單 {ord_:,} 筆"
|
||||
f" · 毛利率 {gm:.1f}% · 客單 NT${aov:,.0f}",
|
||||
3.8, 8.7, 27, 0.85,
|
||||
bold=True, size=14, color=_BRAND_OG2,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
|
||||
pitch_y = 10.2
|
||||
pitch_h = 1.5
|
||||
pitch_w = 27.0
|
||||
|
||||
_add_rect(slide, 3.8, pitch_y, 0.45, pitch_h, "2A7A3F")
|
||||
_add_text(slide, "★ 最大亮點", 4.4, pitch_y + 0.1, pitch_w - 0.7, 0.55,
|
||||
bold=True, size=11, color="2A7A3F",
|
||||
ea_font=_FONT_BODY_EA, latin_font=_FONT_LABEL)
|
||||
_add_text(slide, elevator.get('highlight') or "—",
|
||||
4.4, pitch_y + 0.7, pitch_w - 0.7, 0.75,
|
||||
size=12, color=_DARK_TEXT,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
|
||||
pitch_y2 = pitch_y + pitch_h + 0.4
|
||||
_add_rect(slide, 3.8, pitch_y2, 0.45, pitch_h, "B5342F")
|
||||
_add_text(slide, "⚠ 最大警訊", 4.4, pitch_y2 + 0.1, pitch_w - 0.7, 0.55,
|
||||
bold=True, size=11, color="B5342F",
|
||||
ea_font=_FONT_BODY_EA, latin_font=_FONT_LABEL)
|
||||
_add_text(slide, elevator.get('warning') or "—",
|
||||
4.4, pitch_y2 + 0.7, pitch_w - 0.7, 0.75,
|
||||
size=12, color=_DARK_TEXT,
|
||||
latin_font=_FONT_DISPLAY, ea_font=_FONT_BODY_EA)
|
||||
|
||||
pitch_y3 = pitch_y2 + pitch_h + 0.4
|
||||
mom_rev = elevator.get('mom_rev')
|
||||
if mom_rev is not None:
|
||||
delta_color = "2A7A3F" if mom_rev > 0 else "B5342F"
|
||||
arrow = "▲" if mom_rev > 0 else "▼"
|
||||
_add_rect(slide, 3.8, pitch_y3, 0.45, pitch_h, delta_color)
|
||||
_add_text(slide, "📈 期間動能", 4.4, pitch_y3 + 0.1, pitch_w - 0.7, 0.55,
|
||||
bold=True, size=11, color=delta_color,
|
||||
ea_font=_FONT_BODY_EA, latin_font=_FONT_LABEL)
|
||||
_add_text(slide,
|
||||
f"vs 上期:業績 {arrow} {abs(mom_rev):.1f}%",
|
||||
4.4, pitch_y3 + 0.7, pitch_w - 0.7, 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)
|
||||
return slide
|
||||
|
||||
|
||||
def _vendor_cover_slide(prs, period_lbl, vcount, total_sales, total_profit,
|
||||
avg_margin, pareto_n, risk_label, risk_color):
|
||||
"""廠商報告封面 — 含集中度警示徽章"""
|
||||
|
||||
Reference in New Issue
Block a user