2026-07-23 13:10:50 +08:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
fast_pptx_renderer.py — PPTX 渲染器
|
|
|
|
|
|
接收 DeckContract JSON,按 requiredPageTypes 生成专业 PPTX
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
|
|
|
|
|
使用方法:
|
|
|
|
|
|
python fast_pptx_renderer.py --deck-json <path> --output <path> --theme <theme>
|
|
|
|
|
|
|
|
|
|
|
|
主题:
|
|
|
|
|
|
deepblue - 深海蓝(默认)
|
|
|
|
|
|
caramel - 焦糖色
|
|
|
|
|
|
chinese - 中国红
|
2026-07-29 12:19:26 +08:00
|
|
|
|
business - 商务蓝
|
|
|
|
|
|
minimal - 极简灰
|
|
|
|
|
|
ink - 水墨黑
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
|
|
|
|
|
输出:
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
JSON: { "ok": true, "path": "...", "size": 12345, "slides": 10 }
|
2026-07-23 13:10:50 +08:00
|
|
|
|
"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
from pptx import Presentation
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
from pptx.chart.data import CategoryChartData
|
2026-07-23 13:10:50 +08:00
|
|
|
|
from pptx.dml.color import RGBColor
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION
|
2026-07-23 13:10:50 +08:00
|
|
|
|
from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE
|
|
|
|
|
|
from pptx.enum.text import MSO_ANCHOR, PP_ALIGN
|
|
|
|
|
|
from pptx.util import Inches, Pt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── 主题配色 ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
THEMES = {
|
|
|
|
|
|
"deepblue": {
|
|
|
|
|
|
"bg": RGBColor(10, 28, 50),
|
|
|
|
|
|
"panel": RGBColor(255, 255, 255),
|
|
|
|
|
|
"primary": RGBColor(255, 255, 255),
|
|
|
|
|
|
"text_dark": RGBColor(30, 30, 40),
|
|
|
|
|
|
"accent": RGBColor(24, 137, 141),
|
|
|
|
|
|
"accent_light": RGBColor(200, 230, 230),
|
|
|
|
|
|
"gold": RGBColor(201, 160, 39),
|
|
|
|
|
|
"muted": RGBColor(150, 160, 175),
|
|
|
|
|
|
"line": RGBColor(220, 225, 235),
|
|
|
|
|
|
"good": RGBColor(24, 168, 100),
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
"good_light": RGBColor(180, 230, 200),
|
2026-07-23 13:10:50 +08:00
|
|
|
|
"alert": RGBColor(200, 60, 60),
|
|
|
|
|
|
},
|
|
|
|
|
|
"caramel": {
|
|
|
|
|
|
"bg": RGBColor(248, 245, 239),
|
|
|
|
|
|
"panel": RGBColor(255, 255, 255),
|
|
|
|
|
|
"primary": RGBColor(38, 38, 38),
|
|
|
|
|
|
"text_dark": RGBColor(38, 38, 38),
|
|
|
|
|
|
"accent": RGBColor(206, 138, 44),
|
|
|
|
|
|
"accent_light": RGBColor(240, 230, 208),
|
|
|
|
|
|
"gold": RGBColor(206, 138, 44),
|
|
|
|
|
|
"muted": RGBColor(96, 96, 96),
|
|
|
|
|
|
"line": RGBColor(223, 214, 195),
|
|
|
|
|
|
"good": RGBColor(24, 108, 79),
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
"good_light": RGBColor(165, 204, 189),
|
2026-07-23 13:10:50 +08:00
|
|
|
|
"alert": RGBColor(173, 68, 52),
|
|
|
|
|
|
},
|
|
|
|
|
|
"chinese": {
|
|
|
|
|
|
"bg": RGBColor(180, 30, 30),
|
|
|
|
|
|
"panel": RGBColor(255, 255, 255),
|
|
|
|
|
|
"primary": RGBColor(255, 255, 255),
|
|
|
|
|
|
"text_dark": RGBColor(40, 40, 40),
|
|
|
|
|
|
"accent": RGBColor(220, 180, 60),
|
|
|
|
|
|
"accent_light": RGBColor(255, 240, 200),
|
|
|
|
|
|
"gold": RGBColor(220, 180, 60),
|
|
|
|
|
|
"muted": RGBColor(180, 160, 160),
|
|
|
|
|
|
"line": RGBColor(200, 180, 180),
|
|
|
|
|
|
"good": RGBColor(100, 180, 100),
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
"good_light": RGBColor(200, 230, 200),
|
2026-07-23 13:10:50 +08:00
|
|
|
|
"alert": RGBColor(255, 200, 100),
|
|
|
|
|
|
},
|
2026-07-29 12:19:26 +08:00
|
|
|
|
"business": {
|
|
|
|
|
|
"bg": RGBColor(23, 32, 51),
|
|
|
|
|
|
"panel": RGBColor(255, 255, 255),
|
|
|
|
|
|
"primary": RGBColor(255, 255, 255),
|
|
|
|
|
|
"text_dark": RGBColor(23, 32, 51),
|
|
|
|
|
|
"accent": RGBColor(37, 99, 235),
|
|
|
|
|
|
"accent_light": RGBColor(219, 234, 254),
|
|
|
|
|
|
"gold": RGBColor(201, 160, 39),
|
|
|
|
|
|
"muted": RGBColor(148, 163, 184),
|
|
|
|
|
|
"line": RGBColor(203, 213, 225),
|
|
|
|
|
|
"good": RGBColor(5, 150, 105),
|
|
|
|
|
|
"good_light": RGBColor(209, 250, 229),
|
|
|
|
|
|
"alert": RGBColor(220, 38, 38),
|
|
|
|
|
|
},
|
|
|
|
|
|
"minimal": {
|
|
|
|
|
|
"bg": RGBColor(248, 250, 252),
|
|
|
|
|
|
"panel": RGBColor(255, 255, 255),
|
|
|
|
|
|
"primary": RGBColor(15, 23, 42),
|
|
|
|
|
|
"text_dark": RGBColor(15, 23, 42),
|
|
|
|
|
|
"accent": RGBColor(71, 85, 105),
|
|
|
|
|
|
"accent_light": RGBColor(226, 232, 240),
|
|
|
|
|
|
"gold": RGBColor(161, 98, 7),
|
|
|
|
|
|
"muted": RGBColor(100, 116, 139),
|
|
|
|
|
|
"line": RGBColor(226, 232, 240),
|
|
|
|
|
|
"good": RGBColor(22, 101, 52),
|
|
|
|
|
|
"good_light": RGBColor(220, 252, 231),
|
|
|
|
|
|
"alert": RGBColor(185, 28, 28),
|
|
|
|
|
|
},
|
|
|
|
|
|
"ink": {
|
|
|
|
|
|
"bg": RGBColor(39, 39, 42),
|
|
|
|
|
|
"panel": RGBColor(250, 250, 249),
|
|
|
|
|
|
"primary": RGBColor(250, 250, 249),
|
|
|
|
|
|
"text_dark": RGBColor(39, 39, 42),
|
|
|
|
|
|
"accent": RGBColor(87, 83, 78),
|
|
|
|
|
|
"accent_light": RGBColor(231, 229, 228),
|
|
|
|
|
|
"gold": RGBColor(180, 140, 70),
|
|
|
|
|
|
"muted": RGBColor(168, 162, 158),
|
|
|
|
|
|
"line": RGBColor(214, 211, 209),
|
|
|
|
|
|
"good": RGBColor(63, 98, 18),
|
|
|
|
|
|
"good_light": RGBColor(236, 252, 203),
|
|
|
|
|
|
"alert": RGBColor(153, 27, 27),
|
|
|
|
|
|
},
|
2026-07-23 13:10:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
FONT_CN = "Microsoft YaHei"
|
|
|
|
|
|
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
# ─── 计算工具(移植自 baodanppt) ───────────────────────────
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
|
|
|
|
|
def money(value: float | int) -> str:
|
|
|
|
|
|
return f"{round(float(value)):,}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pct(value: float) -> str:
|
|
|
|
|
|
return f"{value:.2f}%"
|
|
|
|
|
|
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
def paid_premium_for_year(year: int, annual_premium: float, pay_years: int) -> float:
|
|
|
|
|
|
return float(annual_premium) * min(int(year), int(pay_years))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def simple_return(value: float, premium: float) -> float:
|
|
|
|
|
|
if premium <= 0:
|
|
|
|
|
|
return 0.0
|
|
|
|
|
|
return (float(value) / float(premium) - 1.0) * 100.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def compound_return(value: float, premium: float, year: int) -> float:
|
|
|
|
|
|
if premium <= 0 or value <= 0 or year <= 0:
|
|
|
|
|
|
return 0.0
|
|
|
|
|
|
return ((float(value) / float(premium)) ** (1.0 / float(year)) - 1.0) * 100.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def find_payback_year(benefit_rows: list, annual_premium: float, pay_years: int):
|
|
|
|
|
|
for row in benefit_rows:
|
|
|
|
|
|
yr = int(row.get("policyYear", 0))
|
|
|
|
|
|
sv = float(row.get("totalSurrenderValue", 0))
|
|
|
|
|
|
if sv >= paid_premium_for_year(yr, annual_premium, pay_years) and yr > 1:
|
|
|
|
|
|
return yr
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def decade_rows(benefit_rows, annual_premium, pay_years, withdraw_rows=None, max_year=90):
|
|
|
|
|
|
"""每 10 年数据行,支持不提领/提领两种口径。"""
|
|
|
|
|
|
targets = [y for y in [1, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100] if y <= max_year]
|
|
|
|
|
|
by_year = {}
|
|
|
|
|
|
for r in benefit_rows:
|
|
|
|
|
|
by_year[int(r.get("policyYear", 0))] = r
|
|
|
|
|
|
w_by_year = {}
|
|
|
|
|
|
if withdraw_rows:
|
|
|
|
|
|
for r in withdraw_rows:
|
|
|
|
|
|
w_by_year[int(r.get("policyYear", 0))] = r
|
|
|
|
|
|
|
|
|
|
|
|
rows = []
|
|
|
|
|
|
for yr in targets:
|
|
|
|
|
|
base = by_year.get(yr)
|
|
|
|
|
|
wr = w_by_year.get(yr)
|
|
|
|
|
|
if not base and not wr:
|
|
|
|
|
|
continue
|
|
|
|
|
|
ref = wr or base
|
|
|
|
|
|
age = int(ref.get("age", 0))
|
|
|
|
|
|
paid = paid_premium_for_year(yr, annual_premium, pay_years)
|
|
|
|
|
|
if wr:
|
|
|
|
|
|
annual_w = float(wr.get("annualWithdrawal", 0))
|
|
|
|
|
|
cum_w = float(wr.get("cumulativeWithdrawal", 0))
|
|
|
|
|
|
sv_after = float(wr.get("surrenderValueAfter", 0))
|
|
|
|
|
|
econ = cum_w + sv_after
|
|
|
|
|
|
else:
|
|
|
|
|
|
annual_w = 0.0
|
|
|
|
|
|
cum_w = 0.0
|
|
|
|
|
|
sv_after = float(base.get("totalSurrenderValue", 0))
|
|
|
|
|
|
econ = sv_after
|
|
|
|
|
|
rows.append({
|
|
|
|
|
|
"age": age, "year": yr, "paid": paid,
|
|
|
|
|
|
"annualWithdrawal": annual_w, "cumulativeWithdrawal": cum_w,
|
|
|
|
|
|
"surrenderValue": sv_after, "economicTotal": econ,
|
|
|
|
|
|
"simpleRate": simple_return(econ, paid),
|
|
|
|
|
|
"compoundRate": compound_return(econ, paid, yr),
|
|
|
|
|
|
})
|
|
|
|
|
|
return rows
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── 基础绘图函数 ────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def add_bg(slide, colors):
|
2026-07-23 13:10:50 +08:00
|
|
|
|
fill = slide.background.fill
|
|
|
|
|
|
fill.solid()
|
|
|
|
|
|
fill.fore_color.rgb = colors["bg"]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 21:26:48 +08:00
|
|
|
|
def add_blank_slide(prs):
|
|
|
|
|
|
"""兼容只有少量自定义版式的源 PPTX。"""
|
|
|
|
|
|
layouts = list(prs.slide_layouts)
|
|
|
|
|
|
if not layouts:
|
|
|
|
|
|
raise ValueError("源模板没有可用幻灯片版式")
|
|
|
|
|
|
layout = next(
|
|
|
|
|
|
(
|
|
|
|
|
|
item for item in layouts
|
|
|
|
|
|
if str(getattr(item, "name", "")).lower() in {"blank", "空白"}
|
|
|
|
|
|
),
|
|
|
|
|
|
layouts[-1],
|
|
|
|
|
|
)
|
|
|
|
|
|
return prs.slides.add_slide(layout)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-23 13:10:50 +08:00
|
|
|
|
def add_textbox(slide, left, top, width, height, text, size=18, bold=False,
|
|
|
|
|
|
color=None, align=PP_ALIGN.LEFT, line_spacing=1.5, colors=None):
|
|
|
|
|
|
if color is None:
|
|
|
|
|
|
color = colors["primary"] if colors else RGBColor(255, 255, 255)
|
|
|
|
|
|
box = slide.shapes.add_textbox(left, top, width, height)
|
|
|
|
|
|
tf = box.text_frame
|
|
|
|
|
|
tf.clear()
|
|
|
|
|
|
tf.word_wrap = True
|
|
|
|
|
|
tf.vertical_anchor = MSO_ANCHOR.TOP
|
|
|
|
|
|
p = tf.paragraphs[0]
|
|
|
|
|
|
p.alignment = align
|
|
|
|
|
|
p.line_spacing = line_spacing
|
|
|
|
|
|
run = p.add_run()
|
|
|
|
|
|
run.text = str(text)
|
|
|
|
|
|
run.font.name = FONT_CN
|
|
|
|
|
|
run.font.size = Pt(size)
|
|
|
|
|
|
run.font.bold = bold
|
|
|
|
|
|
run.font.color.rgb = color
|
|
|
|
|
|
return box
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_paragraphs(slide, left, top, width, height, lines, size=15, color=None,
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
bullet=True, line_spacing=1.5, colors=None):
|
2026-07-23 13:10:50 +08:00
|
|
|
|
if color is None:
|
|
|
|
|
|
color = colors["primary"] if colors else RGBColor(255, 255, 255)
|
|
|
|
|
|
box = slide.shapes.add_textbox(left, top, width, height)
|
|
|
|
|
|
tf = box.text_frame
|
|
|
|
|
|
tf.clear()
|
|
|
|
|
|
tf.word_wrap = True
|
|
|
|
|
|
tf.vertical_anchor = MSO_ANCHOR.TOP
|
|
|
|
|
|
for idx, line in enumerate(lines):
|
|
|
|
|
|
p = tf.paragraphs[0] if idx == 0 else tf.add_paragraph()
|
|
|
|
|
|
p.alignment = PP_ALIGN.LEFT
|
|
|
|
|
|
p.line_spacing = line_spacing
|
|
|
|
|
|
p.text = f"• {line}" if bullet else str(line)
|
|
|
|
|
|
if p.runs:
|
|
|
|
|
|
run = p.runs[0]
|
|
|
|
|
|
run.font.name = FONT_CN
|
|
|
|
|
|
run.font.size = Pt(size)
|
|
|
|
|
|
run.font.color.rgb = color
|
|
|
|
|
|
return box
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_card(slide, left, top, width, height, title, value, subtitle="",
|
|
|
|
|
|
accent=False, colors=None, title_size=12, value_size=20, subtitle_size=10.5):
|
|
|
|
|
|
if colors is None:
|
|
|
|
|
|
colors = THEMES["deepblue"]
|
|
|
|
|
|
shape = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, left, top, width, height)
|
|
|
|
|
|
shape.fill.solid()
|
|
|
|
|
|
shape.fill.fore_color.rgb = colors["accent_light"] if accent else colors["panel"]
|
|
|
|
|
|
shape.line.color.rgb = colors["line"]
|
|
|
|
|
|
add_textbox(slide, left + Inches(0.14), top + Inches(0.12), width - Inches(0.28),
|
|
|
|
|
|
Inches(0.22), title, size=title_size, bold=True, color=colors["muted"],
|
|
|
|
|
|
line_spacing=1.2, colors=colors)
|
|
|
|
|
|
add_textbox(slide, left + Inches(0.14), top + Inches(0.38), width - Inches(0.28),
|
|
|
|
|
|
Inches(0.4), value, size=value_size, bold=True,
|
|
|
|
|
|
color=colors["accent"] if accent else colors["text_dark"],
|
|
|
|
|
|
line_spacing=1.2, colors=colors)
|
|
|
|
|
|
if subtitle:
|
|
|
|
|
|
add_textbox(slide, left + Inches(0.14), top + Inches(0.82), width - Inches(0.28),
|
|
|
|
|
|
Inches(0.22), subtitle, size=subtitle_size, color=colors["muted"],
|
|
|
|
|
|
line_spacing=1.2, colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
def add_fact_card(slide, left, top, width, height, title, body,
|
|
|
|
|
|
fill_color=None, colors=None, title_size=11.5, body_size=12.8):
|
|
|
|
|
|
if colors is None:
|
|
|
|
|
|
colors = THEMES["deepblue"]
|
|
|
|
|
|
if fill_color is None:
|
|
|
|
|
|
fill_color = colors["panel"]
|
|
|
|
|
|
shape = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, left, top, width, height)
|
|
|
|
|
|
shape.fill.solid()
|
|
|
|
|
|
shape.fill.fore_color.rgb = fill_color
|
|
|
|
|
|
shape.line.color.rgb = colors["line"]
|
|
|
|
|
|
add_textbox(slide, left + Inches(0.14), top + Inches(0.12), width - Inches(0.28),
|
|
|
|
|
|
Inches(0.2), title, size=title_size, bold=True, color=colors["muted"],
|
|
|
|
|
|
line_spacing=1.2, colors=colors)
|
|
|
|
|
|
add_textbox(slide, left + Inches(0.14), top + Inches(0.36), width - Inches(0.28),
|
|
|
|
|
|
height - Inches(0.48), body, size=body_size, color=colors["text_dark"],
|
|
|
|
|
|
line_spacing=1.42, colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-23 13:10:50 +08:00
|
|
|
|
def add_footer(slide, text, colors=None):
|
|
|
|
|
|
if colors is None:
|
|
|
|
|
|
colors = THEMES["deepblue"]
|
|
|
|
|
|
add_textbox(slide, Inches(0.65), Inches(7.0), Inches(12.0), Inches(0.2),
|
|
|
|
|
|
text, size=10, color=colors["muted"], line_spacing=1.2, colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
def add_title(slide, title, subtitle=None, colors=None):
|
|
|
|
|
|
if colors is None:
|
|
|
|
|
|
colors = THEMES["deepblue"]
|
|
|
|
|
|
add_textbox(slide, Inches(0.6), Inches(0.35), Inches(8.8), Inches(0.45),
|
|
|
|
|
|
title, size=24, bold=True, line_spacing=1.2, colors=colors)
|
|
|
|
|
|
if subtitle:
|
|
|
|
|
|
add_textbox(slide, Inches(0.6), Inches(0.78), Inches(9.2), Inches(0.28),
|
|
|
|
|
|
subtitle, size=11, color=colors["muted"], line_spacing=1.2, colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_line_chart(slide, left, top, width, height, categories, series_spec, colors):
|
|
|
|
|
|
chart_data = CategoryChartData()
|
|
|
|
|
|
chart_data.categories = [str(x) for x in categories]
|
|
|
|
|
|
for name, values in series_spec:
|
|
|
|
|
|
chart_data.add_series(name, values)
|
|
|
|
|
|
chart = slide.shapes.add_chart(
|
|
|
|
|
|
XL_CHART_TYPE.LINE_MARKERS, left, top, width, height, chart_data
|
|
|
|
|
|
).chart
|
|
|
|
|
|
chart.has_legend = True
|
|
|
|
|
|
chart.legend.position = XL_LEGEND_POSITION.BOTTOM
|
|
|
|
|
|
chart.legend.font.size = Pt(10)
|
|
|
|
|
|
chart.value_axis.has_major_gridlines = True
|
|
|
|
|
|
chart.value_axis.tick_labels.font.size = Pt(10)
|
|
|
|
|
|
chart.category_axis.tick_labels.font.size = Pt(10)
|
|
|
|
|
|
chart.value_axis.tick_labels.number_format = '$#,##0'
|
|
|
|
|
|
palette = [colors["accent"], colors["good"], RGBColor(54, 94, 140), colors["alert"]]
|
|
|
|
|
|
for idx, series in enumerate(chart.series):
|
|
|
|
|
|
series.format.line.width = Pt(2.2)
|
|
|
|
|
|
series.format.line.color.rgb = palette[idx % len(palette)]
|
|
|
|
|
|
return chart
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_timeline_marker(slide, left, title, lines, colors):
|
|
|
|
|
|
shape = slide.shapes.add_shape(
|
|
|
|
|
|
MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE,
|
|
|
|
|
|
left, Inches(2.75), Inches(2.1), Inches(2.05)
|
|
|
|
|
|
)
|
|
|
|
|
|
shape.fill.solid()
|
|
|
|
|
|
shape.fill.fore_color.rgb = colors["panel"]
|
|
|
|
|
|
shape.line.color.rgb = colors["line"]
|
|
|
|
|
|
add_textbox(slide, left + Inches(0.14), Inches(2.9), Inches(1.82), Inches(0.26),
|
|
|
|
|
|
title, size=13, bold=True, color=colors["accent"],
|
|
|
|
|
|
line_spacing=1.2, colors=colors)
|
|
|
|
|
|
add_paragraphs(slide, left + Inches(0.14), Inches(3.18), Inches(1.82), Inches(1.25),
|
|
|
|
|
|
lines, size=11, bullet=False, line_spacing=1.35, colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── 幻灯片构建函数(10 种 pageType) ───────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def _slide_meta(slides_config, idx, page_type):
|
|
|
|
|
|
"""从 slides_config 中获取第 idx 个 slide 的元数据。"""
|
|
|
|
|
|
if slides_config and idx < len(slides_config):
|
|
|
|
|
|
return slides_config[idx]
|
|
|
|
|
|
return {"pageType": page_type}
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
|
|
|
|
|
|
def _product_summary(product):
|
|
|
|
|
|
"""从产品数据中提取关键数字摘要。"""
|
|
|
|
|
|
policy = product.get("policy", {})
|
|
|
|
|
|
ap = policy.get("annualPremium", 0)
|
|
|
|
|
|
py = policy.get("payYears", 0)
|
|
|
|
|
|
total = ap * py
|
|
|
|
|
|
br = product.get("benefitRows", [])
|
|
|
|
|
|
payback = find_payback_year(br, ap, py) if br else None
|
|
|
|
|
|
final = br[-1].get("totalSurrenderValue", 0) if br else 0
|
|
|
|
|
|
mult = f"{final / total:.1f}x" if total > 0 and final > 0 else "-"
|
|
|
|
|
|
return {"annualPremium": ap, "payYears": py, "totalPremium": total,
|
|
|
|
|
|
"paybackYear": payback, "finalValue": final, "multiple": mult,
|
|
|
|
|
|
"benefitRows": br, "withdrawalRows": product.get("withdrawalRows", []),
|
|
|
|
|
|
"productName": product.get("productName", ""),
|
|
|
|
|
|
"insured": product.get("insured", {})}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 17:24:34 +08:00
|
|
|
|
def _selected_product(deck, meta):
|
|
|
|
|
|
products = deck.get("products", [])
|
|
|
|
|
|
if not products:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
try:
|
|
|
|
|
|
index = int(meta.get("productIndex", 0))
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
index = 0
|
|
|
|
|
|
return products[index] if 0 <= index < len(products) else products[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _source_pages(rows):
|
|
|
|
|
|
pages = sorted({
|
|
|
|
|
|
int(row["sourcePage"])
|
|
|
|
|
|
for row in rows
|
|
|
|
|
|
if row.get("sourcePage")
|
|
|
|
|
|
})
|
|
|
|
|
|
return "、".join(str(page) for page in pages) if pages else "待正式计划书确认"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pending_money(value):
|
|
|
|
|
|
if value is None:
|
|
|
|
|
|
return "待确认"
|
|
|
|
|
|
return money(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
def add_slide_cover(prs, deck, colors, meta):
|
|
|
|
|
|
"""封面页:客户名 + 产品名 + 公司名 + 核心指标卡片。"""
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
customer = deck.get("customer", {})
|
|
|
|
|
|
products = deck.get("products", [])
|
|
|
|
|
|
company = deck.get("company", {})
|
|
|
|
|
|
product = products[0] if products else {}
|
|
|
|
|
|
s = _product_summary(product)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
title_text = meta.get("title", "").replace("{{customerName}}", customer.get("name", "客户"))
|
|
|
|
|
|
if not title_text or "{{" in title_text:
|
|
|
|
|
|
title_text = f"{customer.get('name', '客户')} 专属方案"
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
add_textbox(slide, Inches(0.8), Inches(1.2), Inches(11.7), Inches(0.8),
|
|
|
|
|
|
title_text, size=38, bold=True, colors=colors)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
subtitle = meta.get("subtitle", "").replace("{{productName}}", s["productName"])
|
|
|
|
|
|
if not subtitle or "{{" in subtitle:
|
|
|
|
|
|
subtitle = s["productName"] or "财富增值与传承方案"
|
|
|
|
|
|
add_textbox(slide, Inches(0.8), Inches(2.2), Inches(11.7), Inches(0.5),
|
|
|
|
|
|
subtitle, size=20, color=colors["muted"], colors=colors)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
company_name = company.get("displayName", "")
|
2026-07-23 13:10:50 +08:00
|
|
|
|
if company_name:
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
add_textbox(slide, Inches(0.8), Inches(2.9), Inches(11.7), Inches(0.4),
|
2026-07-23 13:10:50 +08:00
|
|
|
|
company_name, size=14, color=colors["accent"], colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
metrics = [
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
("年缴保费", f"${money(s['annualPremium'])}"),
|
|
|
|
|
|
("缴费年期", f"{s['payYears']}年"),
|
|
|
|
|
|
("总投入", f"${money(s['totalPremium'])}"),
|
|
|
|
|
|
("期末倍数", s["multiple"]),
|
2026-07-23 13:10:50 +08:00
|
|
|
|
]
|
|
|
|
|
|
for i, (label, value) in enumerate(metrics):
|
|
|
|
|
|
x = Inches(0.8 + i * 3.0)
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
add_card(slide, x, Inches(4.8), Inches(2.5), Inches(1.2),
|
2026-07-23 13:10:50 +08:00
|
|
|
|
label, value, colors=colors, accent=(i == 3))
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
add_footer(slide, meta.get("narrativeHint", ""), colors=colors)
|
|
|
|
|
|
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
def add_slide_company(prs, deck, colors, meta):
|
|
|
|
|
|
"""公司介绍页:公司简介 + 事实卡片。"""
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
company = deck.get("company", {})
|
|
|
|
|
|
title = meta.get("title", "").replace("{{companyName}}", company.get("displayName", ""))
|
|
|
|
|
|
if not title or "{{" in title:
|
|
|
|
|
|
title = f"{company.get('displayName', '合作保司')} 公司介绍"
|
|
|
|
|
|
add_title(slide, title, meta.get("narrativeHint", ""), colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
intro = company.get("companyIntro", "")
|
|
|
|
|
|
if intro:
|
|
|
|
|
|
add_paragraphs(slide, Inches(0.85), Inches(1.45), Inches(6.0), Inches(2.5),
|
|
|
|
|
|
[intro], size=13, bullet=False, line_spacing=1.5, colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
highlights = company.get("companyHighlights", [])[:4]
|
|
|
|
|
|
positions = [
|
|
|
|
|
|
(Inches(0.8), Inches(4.2)), (Inches(3.5), Inches(4.2)),
|
|
|
|
|
|
(Inches(6.2), Inches(4.2)), (Inches(8.9), Inches(4.2)),
|
2026-07-23 13:10:50 +08:00
|
|
|
|
]
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
for idx, h in enumerate(highlights):
|
|
|
|
|
|
if idx >= 4:
|
|
|
|
|
|
break
|
|
|
|
|
|
left, top = positions[idx]
|
|
|
|
|
|
text = h.get("text", "") if isinstance(h, dict) else str(h)
|
|
|
|
|
|
source = h.get("sourceFile", "") if isinstance(h, dict) else ""
|
|
|
|
|
|
fill = colors["accent_light"] if idx == 0 else colors["panel"]
|
|
|
|
|
|
add_fact_card(slide, left, top, Inches(2.5), Inches(1.8),
|
|
|
|
|
|
source[:20] if source else f"亮点 {idx+1}", text,
|
|
|
|
|
|
fill_color=fill, colors=colors, body_size=11)
|
|
|
|
|
|
|
|
|
|
|
|
rating = company.get("rating", "")
|
|
|
|
|
|
if rating:
|
|
|
|
|
|
add_textbox(slide, Inches(7.5), Inches(1.45), Inches(5.0), Inches(0.4),
|
|
|
|
|
|
f"评级: {rating}", size=14, bold=True, color=colors["accent"], colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
add_footer(slide, "公司事实来自内部知识库权威口径。", colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_slide_narrative(prs, deck, colors, meta):
|
|
|
|
|
|
"""叙事页:受保人画像 + 方案逻辑 + 产品定位。"""
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
|
2026-07-29 17:24:34 +08:00
|
|
|
|
product = _selected_product(deck, meta)
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
s = _product_summary(product)
|
|
|
|
|
|
insured = s["insured"]
|
|
|
|
|
|
|
|
|
|
|
|
title = meta.get("title", "方案核心逻辑")
|
|
|
|
|
|
add_title(slide, title, meta.get("narrativeHint", ""), colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
age = insured.get("age", "?")
|
|
|
|
|
|
name = insured.get("name", "受保人")
|
|
|
|
|
|
add_fact_card(slide, Inches(0.8), Inches(1.45), Inches(2.5), Inches(1.6),
|
|
|
|
|
|
f"{name} · {age}岁", f"年缴 US${money(s['annualPremium'])}\n缴费 {s['payYears']} 年",
|
|
|
|
|
|
fill_color=colors["accent_light"], colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
if s["paybackYear"]:
|
|
|
|
|
|
add_fact_card(slide, Inches(3.6), Inches(1.45), Inches(2.5), Inches(1.6),
|
|
|
|
|
|
"回本年份", f"约第 {s['paybackYear']} 年", colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
kind = product.get("kind", "savings")
|
|
|
|
|
|
if kind == "savings":
|
|
|
|
|
|
points = [
|
|
|
|
|
|
f"总保费约 US${money(s['totalPremium'])},长期退保价值可持续抬升。",
|
|
|
|
|
|
"它负责未来的确定节奏,用时间换复利。",
|
|
|
|
|
|
]
|
|
|
|
|
|
wr = s["withdrawalRows"]
|
|
|
|
|
|
if wr:
|
|
|
|
|
|
first_w = next((r for r in wr if float(r.get("annualWithdrawal", 0)) > 0), None)
|
|
|
|
|
|
if first_w:
|
|
|
|
|
|
points.insert(0, f"第 {int(first_w['policyYear'])} 年开始每年约 US${money(first_w['annualWithdrawal'])},可按阶段释放现金流。")
|
|
|
|
|
|
elif kind == "ci":
|
|
|
|
|
|
policy = product.get("policy", {})
|
|
|
|
|
|
points = [
|
|
|
|
|
|
f"基础保额 US${money(policy.get('sumInsured', 0))},先把家庭底线守住。",
|
|
|
|
|
|
"这张单的任务不是替代储蓄,而是保护储蓄不被提前动用。",
|
|
|
|
|
|
]
|
|
|
|
|
|
else:
|
|
|
|
|
|
policy = product.get("policy", {})
|
|
|
|
|
|
points = [
|
|
|
|
|
|
f"基础保额 US${money(policy.get('sumInsured', 0))},用有限保费建立更高身故保障。",
|
|
|
|
|
|
"它不是给短期取用,而是给家庭做长期传承和现金价值缓冲。",
|
|
|
|
|
|
]
|
|
|
|
|
|
add_paragraphs(slide, Inches(0.85), Inches(3.5), Inches(11.4), Inches(2.5),
|
|
|
|
|
|
points, size=14.5, bullet=True, colors=colors)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
def add_slide_chart(prs, deck, colors, meta):
|
|
|
|
|
|
"""图表页:python-pptx 原生折线图 + 侧边解读。"""
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
|
2026-07-29 17:24:34 +08:00
|
|
|
|
product = _selected_product(deck, meta)
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
s = _product_summary(product)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
chart_type = meta.get("chartType", "growth")
|
|
|
|
|
|
title = meta.get("title", "价值增长曲线" if chart_type == "growth" else "保证/非保证构成")
|
|
|
|
|
|
add_title(slide, title, meta.get("narrativeHint", ""), colors=colors)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
if chart_type == "stacked":
|
|
|
|
|
|
_draw_stacked_chart(slide, s, colors)
|
|
|
|
|
|
else:
|
|
|
|
|
|
_draw_growth_chart(slide, s, colors)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
def _draw_growth_chart(slide, s, colors):
|
|
|
|
|
|
"""增长曲线图:退保价值 vs 保证价值 vs 已交保费。"""
|
|
|
|
|
|
br = s["benefitRows"]
|
|
|
|
|
|
if not br:
|
|
|
|
|
|
add_paragraphs(slide, Inches(2), Inches(3.5), Inches(8), Inches(1),
|
|
|
|
|
|
["暂无增长数据"], size=16, bullet=False, colors=colors)
|
|
|
|
|
|
return
|
|
|
|
|
|
years = [1, 5, 10, 15, 20, 25, 30, 40, 50]
|
|
|
|
|
|
by_year = {int(r.get("policyYear", 0)): r for r in br}
|
|
|
|
|
|
valid_years = [y for y in years if y in by_year]
|
|
|
|
|
|
if not valid_years:
|
|
|
|
|
|
add_paragraphs(slide, Inches(2), Inches(3.5), Inches(8), Inches(1),
|
|
|
|
|
|
["暂无增长数据"], size=16, bullet=False, colors=colors)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
return
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
series = [
|
|
|
|
|
|
("总退保价值", [float(by_year[y].get("totalSurrenderValue", 0)) for y in valid_years]),
|
|
|
|
|
|
("保证现金价值", [float(by_year[y].get("guaranteedCashValue", 0)) for y in valid_years]),
|
|
|
|
|
|
("已交总保费", [paid_premium_for_year(y, s["annualPremium"], s["payYears"]) for y in valid_years]),
|
|
|
|
|
|
]
|
|
|
|
|
|
add_line_chart(slide, Inches(0.65), Inches(1.45), Inches(6.3), Inches(4.25),
|
|
|
|
|
|
valid_years, series, colors)
|
|
|
|
|
|
|
|
|
|
|
|
notes = []
|
|
|
|
|
|
if s["paybackYear"]:
|
|
|
|
|
|
notes.append(f"回本约第 {s['paybackYear']} 年")
|
|
|
|
|
|
y20 = by_year.get(20)
|
|
|
|
|
|
if y20:
|
|
|
|
|
|
notes.append(f"第20年退保价值 US${money(y20.get('totalSurrenderValue', 0))}")
|
|
|
|
|
|
y30 = by_year.get(30)
|
|
|
|
|
|
if y30:
|
|
|
|
|
|
notes.append(f"第30年退保价值 US${money(y30.get('totalSurrenderValue', 0))}")
|
|
|
|
|
|
notes.append(f"期末倍数约 {s['multiple']}")
|
|
|
|
|
|
|
|
|
|
|
|
add_fact_card(slide, Inches(7.35), Inches(1.6), Inches(5.0), Inches(3.5),
|
|
|
|
|
|
"图表解读", "\n".join(notes), colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _draw_stacked_chart(slide, s, colors):
|
|
|
|
|
|
"""保证/非保证构成图。"""
|
|
|
|
|
|
br = s["benefitRows"]
|
|
|
|
|
|
if not br:
|
|
|
|
|
|
add_paragraphs(slide, Inches(2), Inches(3.5), Inches(8), Inches(1),
|
|
|
|
|
|
["暂无构成数据"], size=16, bullet=False, colors=colors)
|
|
|
|
|
|
return
|
|
|
|
|
|
years = [1, 5, 10, 15, 20, 25, 30, 40, 50]
|
|
|
|
|
|
by_year = {int(r.get("policyYear", 0)): r for r in br}
|
|
|
|
|
|
valid_years = [y for y in years if y in by_year]
|
|
|
|
|
|
if not valid_years:
|
|
|
|
|
|
add_paragraphs(slide, Inches(2), Inches(3.5), Inches(8), Inches(1),
|
|
|
|
|
|
["暂无构成数据"], size=16, bullet=False, colors=colors)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
series = [
|
|
|
|
|
|
("保证现金价值", [float(by_year[y].get("guaranteedCashValue", 0)) for y in valid_years]),
|
|
|
|
|
|
("归原红利", [float(by_year[y].get("reversionaryBonus", 0)) for y in valid_years]),
|
|
|
|
|
|
("终期分红", [float(by_year[y].get("terminalDividend", 0)) for y in valid_years]),
|
|
|
|
|
|
]
|
|
|
|
|
|
add_line_chart(slide, Inches(0.65), Inches(1.45), Inches(6.3), Inches(4.25),
|
|
|
|
|
|
valid_years, series, colors)
|
|
|
|
|
|
|
|
|
|
|
|
add_fact_card(slide, Inches(7.35), Inches(1.6), Inches(5.0), Inches(3.5),
|
|
|
|
|
|
"构成解读", "先看保证底盘,再看非保证弹性,明确长期收益主要来源。",
|
|
|
|
|
|
colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_slide_timeline(prs, deck, colors, meta):
|
|
|
|
|
|
"""时间轴页:横向里程碑。"""
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
|
2026-07-29 17:24:34 +08:00
|
|
|
|
product = _selected_product(deck, meta)
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
s = _product_summary(product)
|
|
|
|
|
|
|
|
|
|
|
|
title = meta.get("title", "家庭时间轴")
|
|
|
|
|
|
add_title(slide, title, meta.get("narrativeHint", ""), colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
# 画横线
|
|
|
|
|
|
line = slide.shapes.add_shape(
|
|
|
|
|
|
MSO_AUTO_SHAPE_TYPE.RECTANGLE,
|
|
|
|
|
|
Inches(1.0), Inches(2.35), Inches(10.8), Inches(0.05)
|
|
|
|
|
|
)
|
|
|
|
|
|
line.fill.solid()
|
|
|
|
|
|
line.fill.fore_color.rgb = colors["line"]
|
|
|
|
|
|
line.line.color.rgb = colors["line"]
|
|
|
|
|
|
|
|
|
|
|
|
wr = {int(r.get("policyYear", 0)): r for r in s["withdrawalRows"]}
|
|
|
|
|
|
insured_age = int(s["insured"].get("age", 1))
|
|
|
|
|
|
|
|
|
|
|
|
milestones = _build_milestones(s, wr, insured_age, meta)
|
|
|
|
|
|
positions = [Inches(0.95), Inches(3.2), Inches(5.45), Inches(7.7), Inches(9.95)]
|
|
|
|
|
|
for i, ms in enumerate(milestones[:5]):
|
|
|
|
|
|
left = positions[i] if i < len(positions) else Inches(0.95 + i * 2.3)
|
|
|
|
|
|
add_timeline_marker(slide, left, ms["title"], ms["lines"], colors)
|
|
|
|
|
|
|
|
|
|
|
|
add_footer(slide, "关键里程碑按受保人年龄和保单年度展开。", colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_milestones(s, wr, insured_age, meta):
|
|
|
|
|
|
"""构建时间轴里程碑。"""
|
|
|
|
|
|
ms_list = []
|
|
|
|
|
|
ms_list.append({"title": "现在", "lines": [f"年缴 US${money(s['annualPremium'])}", "保单生效"]})
|
|
|
|
|
|
|
|
|
|
|
|
# 第一个提领点
|
|
|
|
|
|
first_w = next((r for r in s["withdrawalRows"] if float(r.get("annualWithdrawal", 0)) > 0), None)
|
|
|
|
|
|
if first_w:
|
|
|
|
|
|
wyr = int(first_w["policyYear"])
|
|
|
|
|
|
wage = insured_age + wyr
|
|
|
|
|
|
ms_list.append({"title": f"第{wyr}年({wage}岁)",
|
|
|
|
|
|
"lines": [f"每年约 US${money(first_w['annualWithdrawal'])}", "开始提领"]})
|
|
|
|
|
|
|
|
|
|
|
|
# 18/21岁
|
|
|
|
|
|
for age_m in [18, 21]:
|
|
|
|
|
|
for yr, r in wr.items():
|
|
|
|
|
|
if int(r.get("age", 0)) == age_m:
|
|
|
|
|
|
ms_list.append({"title": f"{age_m}岁(第{yr}年)",
|
|
|
|
|
|
"lines": [f"累计 US${money(r.get('cumulativeWithdrawal', 0))}"]})
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
# 20年/30年
|
|
|
|
|
|
br_by_year = {int(r.get("policyYear", 0)): r for r in s["benefitRows"]}
|
|
|
|
|
|
for yr in [20, 30]:
|
|
|
|
|
|
r = br_by_year.get(yr)
|
|
|
|
|
|
if r:
|
|
|
|
|
|
ms_list.append({"title": f"第{yr}年",
|
|
|
|
|
|
"lines": [f"退保价值 US${money(r.get('totalSurrenderValue', 0))}"]})
|
|
|
|
|
|
|
|
|
|
|
|
return ms_list[:5]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_slide_table(prs, deck, colors, meta):
|
|
|
|
|
|
"""数据表页:每 10 年数据 + 单利/复利 + 侧边解读。"""
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
add_bg(slide, colors)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
2026-07-29 17:24:34 +08:00
|
|
|
|
product = _selected_product(deck, meta)
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
s = _product_summary(product)
|
|
|
|
|
|
|
|
|
|
|
|
table_type = meta.get("tableType", "no_withdraw")
|
|
|
|
|
|
title = meta.get("title", "不提领方案数据表(每10年)" if table_type == "no_withdraw" else "提领方案数据表(每10年)")
|
|
|
|
|
|
add_title(slide, title, meta.get("narrativeHint", ""), colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
has_withdraw = table_type == "withdraw" and s["withdrawalRows"]
|
|
|
|
|
|
rows = decade_rows(s["benefitRows"], s["annualPremium"], s["payYears"],
|
|
|
|
|
|
s["withdrawalRows"] if has_withdraw else None)
|
|
|
|
|
|
if not rows:
|
|
|
|
|
|
add_paragraphs(slide, Inches(1), Inches(3), Inches(10), Inches(1),
|
|
|
|
|
|
["暂无数据"], size=16, bullet=False, colors=colors)
|
|
|
|
|
|
return
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
if has_withdraw:
|
|
|
|
|
|
headers = ["年龄", "年度", "已交保费", "年领金额", "累计领取", "提后现价", "经济总值", "单利", "复利"]
|
|
|
|
|
|
col_widths = [0.6, 0.7, 1.1, 1.0, 1.1, 1.1, 1.1, 0.8, 0.8]
|
|
|
|
|
|
else:
|
|
|
|
|
|
headers = ["年龄", "年度", "已交保费", "退保价值", "单利", "复利"]
|
|
|
|
|
|
col_widths = [0.7, 0.8, 1.3, 1.5, 1.0, 1.0]
|
|
|
|
|
|
|
|
|
|
|
|
n_cols = len(headers)
|
|
|
|
|
|
n_rows = len(rows) + 1
|
|
|
|
|
|
table_width = Inches(sum(col_widths))
|
|
|
|
|
|
table = slide.shapes.add_table(
|
|
|
|
|
|
n_rows, n_cols, Inches(0.55), Inches(1.38), table_width, Inches(min(n_rows * 0.42, 5.0))
|
|
|
|
|
|
).table
|
|
|
|
|
|
|
|
|
|
|
|
for i, w in enumerate(col_widths):
|
|
|
|
|
|
table.columns[i].width = Inches(w)
|
|
|
|
|
|
|
|
|
|
|
|
# 表头
|
|
|
|
|
|
for c, h in enumerate(headers):
|
|
|
|
|
|
cell = table.cell(0, c)
|
|
|
|
|
|
cell.text = h
|
|
|
|
|
|
_style_cell(cell, bold=True, bg=colors["accent"])
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
|
|
|
|
|
# 数据行
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
for r_idx, row in enumerate(rows, 1):
|
|
|
|
|
|
if has_withdraw:
|
|
|
|
|
|
vals = [
|
|
|
|
|
|
str(row["age"]), str(row["year"]),
|
|
|
|
|
|
money(row["paid"]), money(row["annualWithdrawal"]),
|
|
|
|
|
|
money(row["cumulativeWithdrawal"]), money(row["surrenderValue"]),
|
|
|
|
|
|
money(row["economicTotal"]),
|
|
|
|
|
|
pct(row["simpleRate"]), pct(row["compoundRate"]),
|
|
|
|
|
|
]
|
|
|
|
|
|
else:
|
|
|
|
|
|
vals = [
|
|
|
|
|
|
str(row["age"]), str(row["year"]),
|
|
|
|
|
|
money(row["paid"]), money(row["surrenderValue"]),
|
|
|
|
|
|
pct(row["simpleRate"]), pct(row["compoundRate"]),
|
|
|
|
|
|
]
|
|
|
|
|
|
for c, v in enumerate(vals):
|
|
|
|
|
|
cell = table.cell(r_idx, c)
|
|
|
|
|
|
cell.text = v
|
|
|
|
|
|
_style_cell(cell)
|
|
|
|
|
|
|
|
|
|
|
|
# 解读面板
|
|
|
|
|
|
notes = []
|
|
|
|
|
|
if s["paybackYear"]:
|
|
|
|
|
|
notes.append(f"总保费 US${money(s['totalPremium'])},回本约第 {s['paybackYear']} 年")
|
|
|
|
|
|
y20 = next((r for r in rows if r["year"] == 20), None)
|
|
|
|
|
|
if y20:
|
|
|
|
|
|
mult20 = y20["economicTotal"] / s["totalPremium"] if s["totalPremium"] > 0 else 0
|
|
|
|
|
|
notes.append(f"20年约 {mult20:.1f} 倍")
|
|
|
|
|
|
add_fact_card(slide, Inches(10.0), Inches(1.65), Inches(2.7), Inches(3.0),
|
|
|
|
|
|
"解读", "\n".join(notes) if notes else "数据来自官方计划书",
|
|
|
|
|
|
fill_color=colors["accent_light"], colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
add_footer(slide, "表格口径:关键年度每10年展示,数字来自官方计划书标准化结果。", colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _style_cell(cell, bold=False, bg=None):
|
|
|
|
|
|
"""样式化表格单元格。"""
|
|
|
|
|
|
for p in cell.text_frame.paragraphs:
|
|
|
|
|
|
p.alignment = PP_ALIGN.CENTER
|
|
|
|
|
|
for run in p.runs:
|
|
|
|
|
|
run.font.name = FONT_CN
|
|
|
|
|
|
run.font.size = Pt(10)
|
|
|
|
|
|
run.font.bold = bold
|
|
|
|
|
|
if bg:
|
|
|
|
|
|
cell.fill.solid()
|
|
|
|
|
|
cell.fill.fore_color.rgb = bg
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 17:24:34 +08:00
|
|
|
|
def add_slide_guidance(prs, deck, colors, meta):
|
|
|
|
|
|
"""场景叙事页:只展示规则化、面向客户的说明,不补造业务数字。"""
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
2026-07-29 17:24:34 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
add_title(slide, meta.get("title", "方案说明"), meta.get("narrativeHint", ""), colors=colors)
|
|
|
|
|
|
bullets = [str(item) for item in meta.get("bullets", []) if item]
|
|
|
|
|
|
if not bullets:
|
|
|
|
|
|
bullets = ["具体结论以正式计划书及客户确认资料为准。"]
|
|
|
|
|
|
add_paragraphs(
|
|
|
|
|
|
slide,
|
|
|
|
|
|
Inches(1.0),
|
|
|
|
|
|
Inches(1.65),
|
|
|
|
|
|
Inches(11.1),
|
|
|
|
|
|
Inches(4.6),
|
|
|
|
|
|
bullets,
|
|
|
|
|
|
size=18,
|
|
|
|
|
|
bullet=True,
|
|
|
|
|
|
line_spacing=1.65,
|
|
|
|
|
|
colors=colors,
|
|
|
|
|
|
)
|
|
|
|
|
|
add_footer(slide, "缺失数据统一标记待确认,不根据其他情景自行推算。", colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_slide_policy_summary(prs, deck, colors, meta):
|
|
|
|
|
|
"""每份正式计划书对应一页摘要;提取与不提取情景分开。"""
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
2026-07-29 17:24:34 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
product = _selected_product(deck, meta)
|
|
|
|
|
|
summary = _product_summary(product)
|
|
|
|
|
|
mode = meta.get("summaryMode", "no_withdraw")
|
|
|
|
|
|
title = meta.get("title") or f"{summary['productName']} 保单摘要"
|
|
|
|
|
|
add_title(slide, title, summary["productName"], colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
policy = product.get("policy", {})
|
|
|
|
|
|
currency = policy.get("currency") or "USD"
|
|
|
|
|
|
withdrawal_rows = summary["withdrawalRows"]
|
|
|
|
|
|
benefit_rows = summary["benefitRows"]
|
|
|
|
|
|
first_withdraw = next(
|
|
|
|
|
|
(row for row in withdrawal_rows if float(row.get("annualWithdrawal", 0)) > 0),
|
|
|
|
|
|
None,
|
|
|
|
|
|
)
|
|
|
|
|
|
last_withdraw = next(
|
|
|
|
|
|
(row for row in reversed(withdrawal_rows) if float(row.get("annualWithdrawal", 0)) > 0),
|
|
|
|
|
|
None,
|
|
|
|
|
|
)
|
|
|
|
|
|
metrics = [
|
|
|
|
|
|
("投保年龄", f"{summary['insured'].get('age') or '待确认'} 岁"),
|
|
|
|
|
|
("年缴保费", f"{currency} {_pending_money(summary['annualPremium'])}"),
|
|
|
|
|
|
("缴费年限", f"{summary['payYears'] or '待确认'} 年"),
|
|
|
|
|
|
("总计划保费", f"{currency} {_pending_money(summary['totalPremium'])}"),
|
|
|
|
|
|
]
|
|
|
|
|
|
if first_withdraw:
|
|
|
|
|
|
metrics.extend([
|
|
|
|
|
|
("开始提取", f"第 {first_withdraw.get('policyYear')} 保单年度"),
|
|
|
|
|
|
("年度提取", f"{currency} {_pending_money(first_withdraw.get('annualWithdrawal'))}"),
|
|
|
|
|
|
])
|
|
|
|
|
|
for index, (label, value) in enumerate(metrics[:6]):
|
|
|
|
|
|
left = Inches(0.7 + (index % 3) * 4.15)
|
|
|
|
|
|
top = Inches(1.25 + (index // 3) * 0.9)
|
|
|
|
|
|
add_card(
|
|
|
|
|
|
slide,
|
|
|
|
|
|
left,
|
|
|
|
|
|
top,
|
|
|
|
|
|
Inches(3.75),
|
|
|
|
|
|
Inches(0.72),
|
|
|
|
|
|
label,
|
|
|
|
|
|
value,
|
|
|
|
|
|
colors=colors,
|
|
|
|
|
|
title_size=9.5,
|
|
|
|
|
|
value_size=15,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if mode == "withdraw" and withdrawal_rows:
|
|
|
|
|
|
source_rows = withdrawal_rows
|
|
|
|
|
|
milestone_years = {
|
|
|
|
|
|
int(row.get("policyYear", 0))
|
|
|
|
|
|
for row in [first_withdraw, last_withdraw]
|
|
|
|
|
|
if row
|
|
|
|
|
|
} | {10, 20, 30}
|
|
|
|
|
|
rows_by_year = {
|
|
|
|
|
|
int(row.get("policyYear", 0)): row
|
|
|
|
|
|
for row in source_rows
|
|
|
|
|
|
if int(row.get("policyYear", 0)) > 0
|
|
|
|
|
|
}
|
|
|
|
|
|
selected_rows = [
|
|
|
|
|
|
rows_by_year.get(year, {"policyYear": year})
|
|
|
|
|
|
for year in sorted(year for year in milestone_years if year > 0)
|
|
|
|
|
|
]
|
|
|
|
|
|
headers = ["年龄", "保单年度", "年度提取", "累计提取", "提取后价值", "保证价值", "来源页"]
|
|
|
|
|
|
values_for = lambda row: [
|
|
|
|
|
|
str(row.get("age") or "—"),
|
|
|
|
|
|
str(row.get("policyYear") or "—"),
|
|
|
|
|
|
_pending_money(row.get("annualWithdrawal")),
|
|
|
|
|
|
_pending_money(row.get("cumulativeWithdrawal")),
|
|
|
|
|
|
_pending_money(row.get("surrenderValueAfter")),
|
|
|
|
|
|
_pending_money(row.get("guaranteedValueAfter")),
|
|
|
|
|
|
str(row.get("sourcePage") or "待确认"),
|
|
|
|
|
|
]
|
|
|
|
|
|
else:
|
|
|
|
|
|
source_rows = benefit_rows
|
|
|
|
|
|
milestone_years = {summary["payYears"], 10, 20, 30}
|
|
|
|
|
|
rows_by_year = {
|
|
|
|
|
|
int(row.get("policyYear", 0)): row
|
|
|
|
|
|
for row in source_rows
|
|
|
|
|
|
if int(row.get("policyYear", 0)) > 0
|
|
|
|
|
|
}
|
|
|
|
|
|
selected_rows = [
|
|
|
|
|
|
rows_by_year.get(year, {"policyYear": year})
|
|
|
|
|
|
for year in sorted(
|
|
|
|
|
|
int(year) for year in milestone_years
|
|
|
|
|
|
if year is not None and int(year) > 0
|
|
|
|
|
|
)
|
|
|
|
|
|
]
|
|
|
|
|
|
kind = product.get("kind")
|
|
|
|
|
|
if kind == "iul":
|
|
|
|
|
|
headers = ["年龄", "保单年度", "累计保费", "账户/退保价值", "身故保障", "保证现金价值", "来源页"]
|
|
|
|
|
|
values_for = lambda row: [
|
|
|
|
|
|
str(row.get("age") or "—"),
|
|
|
|
|
|
str(row.get("policyYear") or "—"),
|
|
|
|
|
|
_pending_money(row.get("totalPremiumPaid")),
|
|
|
|
|
|
_pending_money(row.get("totalSurrenderValue")),
|
|
|
|
|
|
_pending_money(
|
|
|
|
|
|
row.get("nonGuaranteedDeathBenefit")
|
|
|
|
|
|
or row.get("guaranteedDeathBenefit")
|
|
|
|
|
|
),
|
|
|
|
|
|
_pending_money(row.get("guaranteedCashValue")),
|
|
|
|
|
|
str(row.get("sourcePage") or "待确认"),
|
|
|
|
|
|
]
|
|
|
|
|
|
else:
|
|
|
|
|
|
headers = ["年龄", "保单年度", "累计保费", "保证价值", "非保证利益", "总退保价值", "来源页"]
|
|
|
|
|
|
values_for = lambda row: [
|
|
|
|
|
|
str(row.get("age") or "—"),
|
|
|
|
|
|
str(row.get("policyYear") or "—"),
|
|
|
|
|
|
_pending_money(row.get("totalPremiumPaid")),
|
|
|
|
|
|
_pending_money(row.get("guaranteedCashValue")),
|
|
|
|
|
|
_pending_money(
|
|
|
|
|
|
float(row.get("reversionaryBonus", 0))
|
|
|
|
|
|
+ float(row.get("terminalDividend", 0))
|
|
|
|
|
|
),
|
|
|
|
|
|
_pending_money(row.get("totalSurrenderValue")),
|
|
|
|
|
|
str(row.get("sourcePage") or "待确认"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
if not selected_rows:
|
|
|
|
|
|
add_paragraphs(
|
|
|
|
|
|
slide,
|
|
|
|
|
|
Inches(1.0),
|
|
|
|
|
|
Inches(4.0),
|
|
|
|
|
|
Inches(11.0),
|
|
|
|
|
|
Inches(1.0),
|
|
|
|
|
|
["关键年度数据待正式计划书确认"],
|
|
|
|
|
|
size=17,
|
|
|
|
|
|
bullet=False,
|
|
|
|
|
|
colors=colors,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
table = slide.shapes.add_table(
|
|
|
|
|
|
len(selected_rows) + 1,
|
|
|
|
|
|
len(headers),
|
|
|
|
|
|
Inches(0.55),
|
|
|
|
|
|
Inches(3.15),
|
|
|
|
|
|
Inches(12.2),
|
|
|
|
|
|
Inches(min(0.43 * (len(selected_rows) + 1), 2.8)),
|
|
|
|
|
|
).table
|
|
|
|
|
|
widths = [0.8, 1.0, 1.55, 1.55, 1.7, 1.7, 1.05]
|
|
|
|
|
|
for index, width in enumerate(widths):
|
|
|
|
|
|
table.columns[index].width = Inches(width)
|
|
|
|
|
|
for column, header in enumerate(headers):
|
|
|
|
|
|
table.cell(0, column).text = header
|
|
|
|
|
|
_style_cell(table.cell(0, column), bold=True, bg=colors["accent"])
|
|
|
|
|
|
for row_index, row in enumerate(selected_rows, 1):
|
|
|
|
|
|
for column, value in enumerate(values_for(row)):
|
|
|
|
|
|
table.cell(row_index, column).text = value
|
|
|
|
|
|
_style_cell(table.cell(row_index, column))
|
|
|
|
|
|
|
|
|
|
|
|
add_footer(
|
|
|
|
|
|
slide,
|
|
|
|
|
|
f"数据来源页:{_source_pages(source_rows)};保证与非保证口径不得混用。",
|
|
|
|
|
|
colors=colors,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_slide_comparison_chart(prs, deck, colors, meta):
|
|
|
|
|
|
"""多份储蓄计划按相同保单年度比较总退保价值。"""
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
2026-07-29 17:24:34 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
add_title(slide, meta.get("title", "同年度价值走势"), "", colors=colors)
|
|
|
|
|
|
products = deck.get("products", [])
|
|
|
|
|
|
preferred_years = [5, 10, 20, 25, 30]
|
|
|
|
|
|
series = []
|
|
|
|
|
|
valid_years = []
|
|
|
|
|
|
for year in preferred_years:
|
|
|
|
|
|
if any(
|
|
|
|
|
|
any(int(row.get("policyYear", 0)) == year for row in product.get("benefitRows", []))
|
|
|
|
|
|
for product in products
|
|
|
|
|
|
):
|
|
|
|
|
|
valid_years.append(year)
|
|
|
|
|
|
for product in products:
|
|
|
|
|
|
by_year = {
|
|
|
|
|
|
int(row.get("policyYear", 0)): row
|
|
|
|
|
|
for row in product.get("benefitRows", [])
|
|
|
|
|
|
}
|
|
|
|
|
|
series.append((
|
|
|
|
|
|
product.get("productName") or "未命名计划",
|
|
|
|
|
|
[
|
|
|
|
|
|
(
|
|
|
|
|
|
float(by_year[year].get("totalSurrenderValue", 0))
|
|
|
|
|
|
if year in by_year
|
|
|
|
|
|
else None
|
|
|
|
|
|
)
|
|
|
|
|
|
for year in valid_years
|
|
|
|
|
|
],
|
|
|
|
|
|
))
|
|
|
|
|
|
if valid_years and series:
|
|
|
|
|
|
add_line_chart(
|
|
|
|
|
|
slide,
|
|
|
|
|
|
Inches(0.7),
|
|
|
|
|
|
Inches(1.4),
|
|
|
|
|
|
Inches(11.9),
|
|
|
|
|
|
Inches(4.8),
|
|
|
|
|
|
valid_years,
|
|
|
|
|
|
series,
|
|
|
|
|
|
colors,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
add_paragraphs(
|
|
|
|
|
|
slide,
|
|
|
|
|
|
Inches(1.0),
|
|
|
|
|
|
Inches(3.0),
|
|
|
|
|
|
Inches(11.0),
|
|
|
|
|
|
Inches(1.0),
|
|
|
|
|
|
["缺少共同关键年度,待正式计划书确认"],
|
|
|
|
|
|
size=17,
|
|
|
|
|
|
bullet=False,
|
|
|
|
|
|
colors=colors,
|
|
|
|
|
|
)
|
|
|
|
|
|
add_footer(slide, "只比较正式计划书已提供的相同保单年度。", colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_slide_comparison_table(prs, deck, colors, meta):
|
|
|
|
|
|
"""在同一表中拆分各产品关键年度的保证价值和总演示价值。"""
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
2026-07-29 17:24:34 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
add_title(slide, meta.get("title", "关键年度价值对比"), "", colors=colors)
|
|
|
|
|
|
products = deck.get("products", [])
|
|
|
|
|
|
years = [5, 10, 20, 30]
|
|
|
|
|
|
headers = ["产品"] + [f"第{year}年\n保证/总值" for year in years]
|
|
|
|
|
|
table = slide.shapes.add_table(
|
|
|
|
|
|
len(products) + 1,
|
|
|
|
|
|
len(headers),
|
|
|
|
|
|
Inches(0.55),
|
|
|
|
|
|
Inches(1.55),
|
|
|
|
|
|
Inches(12.2),
|
|
|
|
|
|
Inches(min(0.75 * (len(products) + 1), 4.7)),
|
|
|
|
|
|
).table
|
|
|
|
|
|
table.columns[0].width = Inches(2.6)
|
|
|
|
|
|
for index in range(1, len(headers)):
|
|
|
|
|
|
table.columns[index].width = Inches(2.35)
|
|
|
|
|
|
for column, header in enumerate(headers):
|
|
|
|
|
|
table.cell(0, column).text = header
|
|
|
|
|
|
_style_cell(table.cell(0, column), bold=True, bg=colors["accent"])
|
|
|
|
|
|
for row_index, product in enumerate(products, 1):
|
|
|
|
|
|
by_year = {
|
|
|
|
|
|
int(row.get("policyYear", 0)): row
|
|
|
|
|
|
for row in product.get("benefitRows", [])
|
|
|
|
|
|
}
|
|
|
|
|
|
values = [product.get("productName") or f"方案 {row_index}"]
|
|
|
|
|
|
for year in years:
|
|
|
|
|
|
row = by_year.get(year)
|
|
|
|
|
|
values.append(
|
|
|
|
|
|
"待确认" if not row else
|
|
|
|
|
|
f"{_pending_money(row.get('guaranteedCashValue'))} / "
|
|
|
|
|
|
f"{_pending_money(row.get('totalSurrenderValue'))}"
|
|
|
|
|
|
)
|
|
|
|
|
|
for column, value in enumerate(values):
|
|
|
|
|
|
table.cell(row_index, column).text = value
|
|
|
|
|
|
_style_cell(table.cell(row_index, column), bold=(column == 0))
|
|
|
|
|
|
add_footer(
|
|
|
|
|
|
slide,
|
|
|
|
|
|
"前值为保证现金价值,后值为总演示退保价值;币种和投入条件不一致时不做排名。",
|
|
|
|
|
|
colors=colors,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _portfolio_products(deck):
|
|
|
|
|
|
products = deck.get("products", [])
|
|
|
|
|
|
savings = next((product for product in products if product.get("kind") == "savings"), None)
|
|
|
|
|
|
iul = next((product for product in products if product.get("kind") == "iul"), None)
|
|
|
|
|
|
return savings, iul
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _portfolio_cashflow(deck):
|
|
|
|
|
|
savings, iul = _portfolio_products(deck)
|
|
|
|
|
|
withdrawal = None
|
|
|
|
|
|
if savings:
|
|
|
|
|
|
withdrawal = next(
|
|
|
|
|
|
(
|
|
|
|
|
|
row for row in savings.get("withdrawalRows", [])
|
|
|
|
|
|
if float(row.get("annualWithdrawal", 0)) > 0
|
|
|
|
|
|
),
|
|
|
|
|
|
None,
|
|
|
|
|
|
)
|
|
|
|
|
|
iul_premium = float((iul or {}).get("policy", {}).get("annualPremium", 0))
|
|
|
|
|
|
withdrawal_amount = float((withdrawal or {}).get("annualWithdrawal", 0))
|
|
|
|
|
|
currencies = {
|
|
|
|
|
|
(product.get("policy", {}).get("currency") or "").upper()
|
|
|
|
|
|
for product in (savings, iul)
|
|
|
|
|
|
if product
|
|
|
|
|
|
}
|
|
|
|
|
|
same_currency = len(currencies) == 1
|
|
|
|
|
|
remainder = withdrawal_amount - iul_premium if same_currency and withdrawal and iul_premium else None
|
|
|
|
|
|
return savings, iul, withdrawal, iul_premium, remainder, next(iter(currencies), "USD")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_slide_cashflow_bridge(prs, deck, colors, meta):
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
2026-07-29 17:24:34 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
add_title(slide, meta.get("title", "现金流接力"), "", colors=colors)
|
|
|
|
|
|
_savings, _iul, withdrawal, iul_premium, remainder, currency = _portfolio_cashflow(deck)
|
|
|
|
|
|
items = [
|
|
|
|
|
|
("储蓄计划年度提取", f"{currency} {_pending_money((withdrawal or {}).get('annualWithdrawal'))}"),
|
|
|
|
|
|
("IUL 年度计划保费", f"{currency} {_pending_money(iul_premium or None)}"),
|
|
|
|
|
|
("覆盖保费后年度余量", f"{currency} {_pending_money(remainder)}"),
|
|
|
|
|
|
]
|
|
|
|
|
|
for index, (label, value) in enumerate(items):
|
|
|
|
|
|
add_card(
|
|
|
|
|
|
slide,
|
|
|
|
|
|
Inches(0.8 + index * 4.15),
|
|
|
|
|
|
Inches(2.0),
|
|
|
|
|
|
Inches(3.65),
|
|
|
|
|
|
Inches(1.45),
|
|
|
|
|
|
label,
|
|
|
|
|
|
value,
|
|
|
|
|
|
colors=colors,
|
|
|
|
|
|
accent=(index == 2),
|
|
|
|
|
|
)
|
|
|
|
|
|
add_paragraphs(
|
|
|
|
|
|
slide,
|
|
|
|
|
|
Inches(1.0),
|
|
|
|
|
|
Inches(4.25),
|
|
|
|
|
|
Inches(11.0),
|
|
|
|
|
|
Inches(1.3),
|
|
|
|
|
|
["年度余量 = 正式计划提取金额 − IUL 正式计划保费;币种不一致时不计算。"],
|
|
|
|
|
|
size=16,
|
|
|
|
|
|
bullet=False,
|
|
|
|
|
|
colors=colors,
|
|
|
|
|
|
)
|
|
|
|
|
|
add_footer(slide, "计算只使用两份正式计划书已经提供的金额。", colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_slide_launch_paths(prs, deck, colors, meta):
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
2026-07-29 17:24:34 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
add_title(slide, meta.get("title", "两种启动路径"), "", colors=colors)
|
|
|
|
|
|
_savings, iul, withdrawal, _premium, _remainder, _currency = _portfolio_cashflow(deck)
|
|
|
|
|
|
start_year = int((withdrawal or {}).get("policyYear", 0))
|
|
|
|
|
|
pay_years = int((iul or {}).get("policy", {}).get("payYears", 0))
|
|
|
|
|
|
funding_years = max(0, min(pay_years, start_year - 1)) if start_year else None
|
|
|
|
|
|
early_text = (
|
|
|
|
|
|
f"IUL 从第1年启动;前 {funding_years} 年保费由客户自有资金支付,"
|
|
|
|
|
|
f"第 {start_year} 年起才可由储蓄提取承接。"
|
|
|
|
|
|
if funding_years is not None else
|
|
|
|
|
|
"IUL 从第1年启动;储蓄正式提取前的保费由客户自有资金支付,具体年数待确认。"
|
|
|
|
|
|
)
|
|
|
|
|
|
sync_text = (
|
|
|
|
|
|
f"前期先完成储蓄安排;第 {start_year} 年正式开始提取时再启动 IUL。"
|
|
|
|
|
|
if start_year else
|
|
|
|
|
|
"前期先完成储蓄安排;待正式计划书确认可提取年度后再启动 IUL。"
|
|
|
|
|
|
)
|
|
|
|
|
|
add_fact_card(
|
|
|
|
|
|
slide,
|
|
|
|
|
|
Inches(0.8),
|
|
|
|
|
|
Inches(1.55),
|
|
|
|
|
|
Inches(5.65),
|
|
|
|
|
|
Inches(3.7),
|
|
|
|
|
|
"路径一|保障先行",
|
|
|
|
|
|
early_text + "\n优点:保障建立较早。\n代价:前期现金流支出较多。",
|
|
|
|
|
|
fill_color=colors["accent_light"],
|
|
|
|
|
|
colors=colors,
|
|
|
|
|
|
body_size=14,
|
|
|
|
|
|
)
|
|
|
|
|
|
add_fact_card(
|
|
|
|
|
|
slide,
|
|
|
|
|
|
Inches(6.85),
|
|
|
|
|
|
Inches(1.55),
|
|
|
|
|
|
Inches(5.65),
|
|
|
|
|
|
Inches(3.7),
|
|
|
|
|
|
"路径二|现金流同步",
|
|
|
|
|
|
sync_text + "\n优点:无需额外准备前期 IUL 保费。\n代价:保障建立时间较晚。",
|
|
|
|
|
|
fill_color=colors["good_light"],
|
|
|
|
|
|
colors=colors,
|
|
|
|
|
|
body_size=14,
|
|
|
|
|
|
)
|
|
|
|
|
|
add_footer(slide, "两条路径没有绝对好坏,选择取决于保障紧迫度和前期现金流。", colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_slide_combined_summary(prs, deck, colors, meta):
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
2026-07-29 17:24:34 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
add_title(slide, meta.get("title", "组合保单摘要"), "", colors=colors)
|
|
|
|
|
|
savings, iul, withdrawal, iul_premium, remainder, currency = _portfolio_cashflow(deck)
|
|
|
|
|
|
savings_summary = _product_summary(savings or {})
|
|
|
|
|
|
iul_summary = _product_summary(iul or {})
|
|
|
|
|
|
blocks = [
|
|
|
|
|
|
(
|
|
|
|
|
|
"储蓄计划",
|
|
|
|
|
|
f"年缴 {currency} {_pending_money(savings_summary['annualPremium'])} × "
|
|
|
|
|
|
f"{savings_summary['payYears'] or '待确认'} 年\n"
|
|
|
|
|
|
f"开始提取:第 {(withdrawal or {}).get('policyYear') or '待确认'} 保单年度\n"
|
|
|
|
|
|
f"年度提取:{currency} {_pending_money((withdrawal or {}).get('annualWithdrawal'))}",
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
"IUL 计划",
|
|
|
|
|
|
f"年缴 {currency} {_pending_money(iul_summary['annualPremium'])} × "
|
|
|
|
|
|
f"{iul_summary['payYears'] or '待确认'} 年\n"
|
|
|
|
|
|
f"总计划保费:{currency} {_pending_money(iul_summary['totalPremium'])}\n"
|
|
|
|
|
|
f"初始身故保障:{currency} "
|
|
|
|
|
|
f"{_pending_money((iul or {}).get('policy', {}).get('sumInsured'))}",
|
|
|
|
|
|
),
|
|
|
|
|
|
(
|
|
|
|
|
|
"组合结果",
|
|
|
|
|
|
f"IUL 年度保费:{currency} {_pending_money(iul_premium or None)}\n"
|
|
|
|
|
|
f"覆盖后年度余量:{currency} {_pending_money(remainder)}\n"
|
|
|
|
|
|
f"启动年度是否一致:{'是' if (withdrawal or {}).get('policyYear') == 1 else '否'}",
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
for index, (label, body) in enumerate(blocks):
|
|
|
|
|
|
add_fact_card(
|
|
|
|
|
|
slide,
|
|
|
|
|
|
Inches(0.65 + index * 4.2),
|
|
|
|
|
|
Inches(1.65),
|
|
|
|
|
|
Inches(3.85),
|
|
|
|
|
|
Inches(3.75),
|
|
|
|
|
|
label,
|
|
|
|
|
|
body,
|
|
|
|
|
|
fill_color=colors["accent_light"] if index == 2 else colors["panel"],
|
|
|
|
|
|
colors=colors,
|
|
|
|
|
|
body_size=13.2,
|
|
|
|
|
|
)
|
|
|
|
|
|
add_footer(slide, "组合总投入仅在币种一致时具有可加总意义。", colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_slide_alignment_table(prs, deck, colors, meta):
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
2026-07-29 17:24:34 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
add_title(slide, meta.get("title", "两份计划书对应表"), "", colors=colors)
|
|
|
|
|
|
savings, iul, withdrawal, _premium, _remainder, _currency = _portfolio_cashflow(deck)
|
|
|
|
|
|
milestones = {
|
|
|
|
|
|
1,
|
|
|
|
|
|
5,
|
|
|
|
|
|
10,
|
|
|
|
|
|
20,
|
|
|
|
|
|
30,
|
|
|
|
|
|
int((withdrawal or {}).get("policyYear", 0)),
|
|
|
|
|
|
}
|
|
|
|
|
|
years = sorted(year for year in milestones if year > 0)
|
|
|
|
|
|
savings_benefits = {
|
|
|
|
|
|
int(row.get("policyYear", 0)): row
|
|
|
|
|
|
for row in (savings or {}).get("benefitRows", [])
|
|
|
|
|
|
}
|
|
|
|
|
|
savings_withdrawals = {
|
|
|
|
|
|
int(row.get("policyYear", 0)): row
|
|
|
|
|
|
for row in (savings or {}).get("withdrawalRows", [])
|
|
|
|
|
|
}
|
|
|
|
|
|
iul_benefits = {
|
|
|
|
|
|
int(row.get("policyYear", 0)): row
|
|
|
|
|
|
for row in (iul or {}).get("benefitRows", [])
|
|
|
|
|
|
}
|
|
|
|
|
|
headers = ["保单年度", "储蓄累计保费", "储蓄计划提取", "储蓄退保价值", "IUL累计保费", "IUL账户价值", "IUL身故保障"]
|
|
|
|
|
|
table = slide.shapes.add_table(
|
|
|
|
|
|
len(years) + 1,
|
|
|
|
|
|
len(headers),
|
|
|
|
|
|
Inches(0.35),
|
|
|
|
|
|
Inches(1.35),
|
|
|
|
|
|
Inches(12.6),
|
|
|
|
|
|
Inches(min(0.52 * (len(years) + 1), 4.9)),
|
|
|
|
|
|
).table
|
|
|
|
|
|
widths = [1.0, 1.65, 1.65, 1.65, 1.65, 1.65, 1.75]
|
|
|
|
|
|
for index, width in enumerate(widths):
|
|
|
|
|
|
table.columns[index].width = Inches(width)
|
|
|
|
|
|
for column, header in enumerate(headers):
|
|
|
|
|
|
table.cell(0, column).text = header
|
|
|
|
|
|
_style_cell(table.cell(0, column), bold=True, bg=colors["accent"])
|
|
|
|
|
|
for row_index, year in enumerate(years, 1):
|
|
|
|
|
|
savings_row = savings_benefits.get(year, {})
|
|
|
|
|
|
withdrawal_row = savings_withdrawals.get(year, {})
|
|
|
|
|
|
iul_row = iul_benefits.get(year, {})
|
|
|
|
|
|
values = [
|
|
|
|
|
|
str(year),
|
|
|
|
|
|
_pending_money(savings_row.get("totalPremiumPaid")),
|
|
|
|
|
|
_pending_money(withdrawal_row.get("annualWithdrawal")),
|
|
|
|
|
|
_pending_money(
|
|
|
|
|
|
withdrawal_row.get("surrenderValueAfter")
|
|
|
|
|
|
if withdrawal_row else savings_row.get("totalSurrenderValue")
|
|
|
|
|
|
),
|
|
|
|
|
|
_pending_money(iul_row.get("totalPremiumPaid")),
|
|
|
|
|
|
_pending_money(iul_row.get("totalSurrenderValue")),
|
|
|
|
|
|
_pending_money(
|
|
|
|
|
|
iul_row.get("nonGuaranteedDeathBenefit")
|
|
|
|
|
|
or iul_row.get("guaranteedDeathBenefit")
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
for column, value in enumerate(values):
|
|
|
|
|
|
table.cell(row_index, column).text = value
|
|
|
|
|
|
_style_cell(table.cell(row_index, column), bold=(column == 0))
|
|
|
|
|
|
add_footer(
|
|
|
|
|
|
slide,
|
|
|
|
|
|
"以保单年度和现金流发生年度为主对应;不同受保人年龄不强行对齐。",
|
|
|
|
|
|
colors=colors,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
def add_slide_compare(prs, deck, colors, meta):
|
2026-07-30 10:30:33 +08:00
|
|
|
|
"""对比页:多产品对比,支持 1~10 份。"""
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
products = deck.get("products", [])
|
|
|
|
|
|
title = meta.get("title", "产品对比")
|
|
|
|
|
|
add_title(slide, title, meta.get("narrativeHint", ""), colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
if len(products) < 2:
|
|
|
|
|
|
# 单产品时显示保证 vs 非保证对比
|
|
|
|
|
|
product = products[0] if products else {}
|
|
|
|
|
|
s = _product_summary(product)
|
|
|
|
|
|
add_fact_card(slide, Inches(0.8), Inches(1.45), Inches(5.45), Inches(2.5),
|
|
|
|
|
|
"保证部分", f"保证现金价值\n回本约第 {s['paybackYear'] or '?'} 年\n确定性高",
|
|
|
|
|
|
fill_color=colors["accent_light"], colors=colors)
|
|
|
|
|
|
add_fact_card(slide, Inches(6.95), Inches(1.45), Inches(5.45), Inches(2.5),
|
|
|
|
|
|
"非保证部分", f"归原红利 + 终期分红\n长期弹性空间大\n取决于公司投资表现",
|
|
|
|
|
|
fill_color=colors["good_light"], colors=colors)
|
|
|
|
|
|
add_paragraphs(slide, Inches(0.85), Inches(4.5), Inches(11.4), Inches(1.5),
|
|
|
|
|
|
["保证部分是底线,非保证部分是弹性。两者合计才是长期回报的完整图景。"],
|
|
|
|
|
|
size=15, bullet=True, colors=colors)
|
2026-07-30 10:30:33 +08:00
|
|
|
|
elif len(products) <= 4:
|
|
|
|
|
|
# 2~4 产品:卡片网格布局
|
|
|
|
|
|
card_w = Inches(5.45) if len(products) == 2 else Inches(5.45)
|
|
|
|
|
|
card_h = Inches(2.5) if len(products) <= 2 else Inches(2.0)
|
|
|
|
|
|
fill_colors = [colors["accent_light"], colors["good_light"],
|
|
|
|
|
|
colors["accent_light"], colors["good_light"]]
|
|
|
|
|
|
positions = [
|
|
|
|
|
|
(Inches(0.8), Inches(1.45)),
|
|
|
|
|
|
(Inches(6.95), Inches(1.45)),
|
|
|
|
|
|
(Inches(0.8), Inches(3.7)),
|
|
|
|
|
|
(Inches(6.95), Inches(3.7)),
|
|
|
|
|
|
]
|
|
|
|
|
|
for i, product in enumerate(products):
|
|
|
|
|
|
s = _product_summary(product)
|
|
|
|
|
|
x, y = positions[i]
|
|
|
|
|
|
add_fact_card(slide, x, y, card_w, card_h,
|
|
|
|
|
|
s["productName"] or f"产品 {i + 1}",
|
|
|
|
|
|
f"年缴 ${money(s['annualPremium'])}\n"
|
|
|
|
|
|
f"总投入 ${money(s['totalPremium'])}\n"
|
|
|
|
|
|
f"回本约第 {s['paybackYear'] or '?'} 年",
|
|
|
|
|
|
fill_color=fill_colors[i % len(fill_colors)], colors=colors)
|
|
|
|
|
|
bottom_y = Inches(6.0) if len(products) <= 2 else Inches(6.0)
|
|
|
|
|
|
add_paragraphs(slide, Inches(0.85), bottom_y, Inches(11.4), Inches(1.0),
|
|
|
|
|
|
["对比重点在相同保单年度下的保证价值、总退保价值和回本时间。"],
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
size=15, bullet=True, colors=colors)
|
2026-07-30 10:30:33 +08:00
|
|
|
|
else:
|
|
|
|
|
|
# 5+ 产品:表格布局
|
|
|
|
|
|
from pptx.util import Emu
|
|
|
|
|
|
rows = len(products) + 1
|
|
|
|
|
|
cols = 5
|
|
|
|
|
|
table_left, table_top = Inches(0.6), Inches(1.5)
|
|
|
|
|
|
table_w, table_h = Inches(12.0), Inches(0.4 * rows + 0.2)
|
|
|
|
|
|
table_shape = slide.shapes.add_table(rows, cols, table_left, table_top, table_w, table_h)
|
|
|
|
|
|
table = table_shape.table
|
|
|
|
|
|
headers = ["产品", "年缴保费", "总投入", "回本年", "期末倍数"]
|
|
|
|
|
|
for c, h in enumerate(headers):
|
|
|
|
|
|
table.cell(0, c).text = h
|
|
|
|
|
|
_style_cell(table.cell(0, c), bold=True, bg=colors.get("accent", RGBColor(59, 122, 87)))
|
|
|
|
|
|
for r, product in enumerate(products, 1):
|
|
|
|
|
|
s = _product_summary(product)
|
|
|
|
|
|
values = [
|
|
|
|
|
|
s["productName"] or f"产品 {r}",
|
|
|
|
|
|
f"${money(s['annualPremium'])}",
|
|
|
|
|
|
f"${money(s['totalPremium'])}",
|
|
|
|
|
|
str(s["paybackYear"] or "-"),
|
|
|
|
|
|
s["multiple"],
|
|
|
|
|
|
]
|
|
|
|
|
|
for c, v in enumerate(values):
|
|
|
|
|
|
table.cell(r, c).text = v
|
|
|
|
|
|
_style_cell(table.cell(r, c), bold=(c == 0))
|
|
|
|
|
|
add_paragraphs(slide, Inches(0.85), Inches(5.5), Inches(11.4), Inches(1.0),
|
|
|
|
|
|
["以上为各产品在相同口径下的核心指标汇总,具体差异请结合正式计划书逐项核对。"],
|
|
|
|
|
|
size=14, bullet=True, colors=colors)
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
|
|
|
|
|
|
add_footer(slide, "", colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_slide_synergy(prs, deck, colors, meta):
|
2026-07-30 10:30:33 +08:00
|
|
|
|
"""协同关系页:多产品如何配合,支持 N 份。"""
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
|
|
|
|
|
|
products = deck.get("products", [])
|
|
|
|
|
|
title = meta.get("title", "协同关系")
|
|
|
|
|
|
add_title(slide, title, meta.get("narrativeHint", "功能分层,互不冲突"), colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
if len(products) >= 2:
|
2026-07-30 10:30:33 +08:00
|
|
|
|
fill_colors = [colors["accent_light"], colors["good_light"],
|
|
|
|
|
|
colors["accent_light"], colors["good_light"]]
|
|
|
|
|
|
if len(products) <= 2:
|
|
|
|
|
|
# 双产品:左右卡片
|
|
|
|
|
|
left_p = _product_summary(products[0])
|
|
|
|
|
|
right_p = _product_summary(products[1])
|
|
|
|
|
|
left_kind = products[0].get("kind", "savings")
|
|
|
|
|
|
right_kind = products[1].get("kind", "savings")
|
|
|
|
|
|
left_label = _kind_label(left_kind)
|
|
|
|
|
|
right_label = _kind_label(right_kind)
|
|
|
|
|
|
add_fact_card(slide, Inches(0.8), Inches(1.45), Inches(5.45), Inches(2.05),
|
|
|
|
|
|
f"{left_label} 负责", _kind_synergy_left(left_kind, left_p),
|
|
|
|
|
|
fill_color=colors["accent_light"], colors=colors)
|
|
|
|
|
|
add_fact_card(slide, Inches(6.95), Inches(1.45), Inches(5.45), Inches(2.05),
|
|
|
|
|
|
f"{right_label} 负责", _kind_synergy_right(right_kind, right_p),
|
|
|
|
|
|
fill_color=colors["good_light"], colors=colors)
|
|
|
|
|
|
add_paragraphs(slide, Inches(0.85), Inches(4.0), Inches(11.3), Inches(2.0),
|
|
|
|
|
|
[f"{left_label}和{right_label}不是重复配置,而是功能分层。",
|
|
|
|
|
|
"先看谁扛风险,再看谁承接未来目标。"],
|
|
|
|
|
|
size=15, bullet=True, colors=colors)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 3+ 产品:垂直列表
|
|
|
|
|
|
card_h = min(1.3, 4.5 / len(products))
|
|
|
|
|
|
for i, product in enumerate(products):
|
|
|
|
|
|
s = _product_summary(product)
|
|
|
|
|
|
kind = product.get("kind", "savings")
|
|
|
|
|
|
label = _kind_label(kind)
|
|
|
|
|
|
y = 1.45 + i * (card_h + 0.15)
|
|
|
|
|
|
body = _kind_synergy_left(kind, s) if i % 2 == 0 else _kind_synergy_right(kind, s)
|
|
|
|
|
|
add_fact_card(slide, Inches(0.8), Inches(y), Inches(11.5), Inches(card_h),
|
|
|
|
|
|
f"{label}|{s['productName'] or f'产品 {i + 1}'}",
|
|
|
|
|
|
body.replace("\n", " · "),
|
|
|
|
|
|
fill_color=fill_colors[i % len(fill_colors)], colors=colors)
|
|
|
|
|
|
add_paragraphs(slide, Inches(0.85), Inches(5.8), Inches(11.3), Inches(0.8),
|
|
|
|
|
|
["各产品按功能分层配置,不重复承担相同风险。"],
|
|
|
|
|
|
size=15, bullet=True, colors=colors)
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
else:
|
|
|
|
|
|
add_paragraphs(slide, Inches(1), Inches(3), Inches(10), Inches(2),
|
|
|
|
|
|
["单产品方案,无需展示协同关系。"],
|
|
|
|
|
|
size=16, bullet=False, colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
add_footer(slide, "", colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _kind_label(kind):
|
|
|
|
|
|
return {"savings": "储蓄险", "ci": "重疾险", "iul": "IUL"}.get(kind, "产品")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _kind_synergy_left(kind, s):
|
|
|
|
|
|
if kind == "ci":
|
|
|
|
|
|
return "风险来时,用重疾险扛支出\n储蓄险继续留给家庭\n教育金目标不被打断"
|
|
|
|
|
|
if kind == "iul":
|
|
|
|
|
|
return "更高身故保障底盘\n家族传承效率\n晚年流动性与应急缓冲"
|
|
|
|
|
|
return f"未来现金流节奏\n回本约第 {s['paybackYear'] or '?'} 年\n为教育/成长阶段提供稳健现金流"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _kind_synergy_right(kind, s):
|
|
|
|
|
|
if kind == "ci":
|
|
|
|
|
|
return "家庭防火墙\n先扛风险支出\n保护储蓄不被提前动用"
|
|
|
|
|
|
if kind == "iul":
|
|
|
|
|
|
return "长期身故杠杆\n代际传承效率\n现金价值缓冲"
|
|
|
|
|
|
return f"长期财富累积\n期末倍数 {s['multiple']}\n时间换复利"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_slide_conclusion(prs, deck, colors, meta):
|
2026-07-30 10:30:33 +08:00
|
|
|
|
"""结论页:总结 + 核心数据,支持 N 份产品。"""
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
add_bg(slide, colors)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
products = deck.get("products", [])
|
|
|
|
|
|
title = meta.get("title", "结论")
|
|
|
|
|
|
add_title(slide, title, meta.get("narrativeHint", ""), colors=colors)
|
|
|
|
|
|
|
2026-07-30 10:30:33 +08:00
|
|
|
|
if len(products) <= 1:
|
|
|
|
|
|
product = products[0] if products else {}
|
|
|
|
|
|
s = _product_summary(product)
|
|
|
|
|
|
points = []
|
|
|
|
|
|
if s["paybackYear"]:
|
|
|
|
|
|
points.append(f"总投入 ${money(s['totalPremium'])},回本约第 {s['paybackYear']} 年")
|
|
|
|
|
|
points.append(f"期末退保价值约 ${money(s['finalValue'])},倍数 {s['multiple']}")
|
|
|
|
|
|
add_paragraphs(slide, Inches(0.85), Inches(1.5), Inches(6.0), Inches(3.0),
|
|
|
|
|
|
points, size=15, bullet=True, colors=colors)
|
|
|
|
|
|
cards = [
|
|
|
|
|
|
("总投入", f"${money(s['totalPremium'])}"),
|
|
|
|
|
|
("期末价值", f"${money(s['finalValue'])}"),
|
|
|
|
|
|
("倍数", s["multiple"]),
|
|
|
|
|
|
]
|
|
|
|
|
|
for i, (label, value) in enumerate(cards):
|
|
|
|
|
|
x = Inches(0.9 + i * 2.8)
|
|
|
|
|
|
add_card(slide, x, Inches(5.0), Inches(2.4), Inches(1.1),
|
|
|
|
|
|
label, value, colors=colors, accent=(i == 2))
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 多产品:逐产品摘要 + 总览卡片
|
|
|
|
|
|
lines = []
|
|
|
|
|
|
for i, product in enumerate(products):
|
|
|
|
|
|
s = _product_summary(product)
|
|
|
|
|
|
name = s["productName"] or f"产品 {i + 1}"
|
|
|
|
|
|
line = f"{name}:投入 ${money(s['totalPremium'])},回本第 {s['paybackYear'] or '?'} 年,倍数 {s['multiple']}"
|
|
|
|
|
|
lines.append(line)
|
|
|
|
|
|
if len(products) >= 2:
|
|
|
|
|
|
lines.append("组合方案把家庭资产目标拆成不同的功能层")
|
|
|
|
|
|
add_paragraphs(slide, Inches(0.85), Inches(1.5), Inches(11.4), Inches(3.0),
|
|
|
|
|
|
lines, size=14, bullet=True, colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
# 指标卡片:展示总投入和产品数
|
|
|
|
|
|
total_investment = sum(_product_summary(p)["totalPremium"] for p in products)
|
|
|
|
|
|
cards = [
|
|
|
|
|
|
("计划书数量", f"{len(products)} 份"),
|
|
|
|
|
|
("总投入", f"${money(total_investment)}"),
|
|
|
|
|
|
("对比维度", "按保单年度"),
|
|
|
|
|
|
]
|
|
|
|
|
|
for i, (label, value) in enumerate(cards):
|
|
|
|
|
|
x = Inches(0.9 + i * 3.8)
|
|
|
|
|
|
add_card(slide, x, Inches(5.0), Inches(3.2), Inches(1.1),
|
|
|
|
|
|
label, value, colors=colors, accent=(i == 2))
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
add_footer(slide, "", colors=colors)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
def add_slide_closing(prs, deck, colors, meta):
|
|
|
|
|
|
"""结束页:感谢 + 声明。"""
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slide = add_blank_slide(prs)
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
add_bg(slide, colors)
|
|
|
|
|
|
|
|
|
|
|
|
title = meta.get("title", "感谢信任")
|
|
|
|
|
|
add_textbox(slide, Inches(0.8), Inches(2.0), Inches(11.7), Inches(1.0),
|
|
|
|
|
|
title, size=36, bold=True, align=PP_ALIGN.CENTER, colors=colors)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
narrative = meta.get("narrativeHint", "方案可继续迭代优化,如有疑问请随时联系。")
|
|
|
|
|
|
add_textbox(slide, Inches(1.5), Inches(3.5), Inches(10.3), Inches(1.0),
|
|
|
|
|
|
narrative, size=18, color=colors["muted"],
|
2026-07-23 13:10:50 +08:00
|
|
|
|
align=PP_ALIGN.CENTER, colors=colors)
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
add_paragraphs(slide, Inches(2.5), Inches(5.0), Inches(8.3), Inches(1.5),
|
|
|
|
|
|
["本方案用于沟通理解,最终权益以保险公司正式文件为准。",
|
|
|
|
|
|
"建议尽快与您的保险经纪人预约时间完成方案确认。"],
|
|
|
|
|
|
size=12, bullet=True, color=colors["muted"], colors=colors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── pageType → builder 映射 ────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
SLIDE_BUILDERS = {
|
|
|
|
|
|
"cover": add_slide_cover,
|
|
|
|
|
|
"company": add_slide_company,
|
|
|
|
|
|
"narrative": add_slide_narrative,
|
2026-07-29 17:24:34 +08:00
|
|
|
|
"guidance": add_slide_guidance,
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
"chart": add_slide_chart,
|
2026-07-29 17:24:34 +08:00
|
|
|
|
"comparison_chart": add_slide_comparison_chart,
|
|
|
|
|
|
"comparison_table": add_slide_comparison_table,
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
"timeline": add_slide_timeline,
|
|
|
|
|
|
"table": add_slide_table,
|
2026-07-29 17:24:34 +08:00
|
|
|
|
"policy_summary": add_slide_policy_summary,
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
"compare": add_slide_compare,
|
|
|
|
|
|
"synergy": add_slide_synergy,
|
2026-07-29 17:24:34 +08:00
|
|
|
|
"cashflow_bridge": add_slide_cashflow_bridge,
|
|
|
|
|
|
"launch_paths": add_slide_launch_paths,
|
|
|
|
|
|
"combined_summary": add_slide_combined_summary,
|
|
|
|
|
|
"alignment_table": add_slide_alignment_table,
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
"conclusion": add_slide_conclusion,
|
|
|
|
|
|
"closing": add_slide_closing,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 默认 pageType 序列(当 DeckContract 无 templateConfig 时使用)
|
|
|
|
|
|
DEFAULT_PAGE_TYPES = {
|
|
|
|
|
|
"savings": ["cover", "company", "narrative", "chart", "chart", "timeline", "timeline", "table", "table", "conclusion", "closing"],
|
|
|
|
|
|
"ci": ["cover", "company", "narrative", "chart", "table", "conclusion", "closing"],
|
|
|
|
|
|
"iul": ["cover", "company", "narrative", "chart", "compare", "table", "conclusion", "closing"],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_page_types(deck):
|
|
|
|
|
|
"""从 DeckContract 中解析要渲染的页面类型列表。"""
|
2026-07-29 17:24:34 +08:00
|
|
|
|
scenario_slides = deck.get("scenarioSlides", [])
|
|
|
|
|
|
if scenario_slides:
|
|
|
|
|
|
return [slide["pageType"] for slide in scenario_slides]
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
tc = deck.get("templateConfig", {})
|
|
|
|
|
|
# 优先从 slidesConfig(管理员配置的逐页数据)中提取顺序
|
|
|
|
|
|
slides_cfg = tc.get("slidesConfig", [])
|
|
|
|
|
|
if slides_cfg and isinstance(slides_cfg, list) and len(slides_cfg) > 0:
|
|
|
|
|
|
if isinstance(slides_cfg[0], dict) and "pageType" in slides_cfg[0]:
|
|
|
|
|
|
return [s["pageType"] for s in slides_cfg]
|
|
|
|
|
|
# 其次从 requiredPageTypes(JSON seed 数据)中读取
|
|
|
|
|
|
page_types = tc.get("requiredPageTypes", [])
|
|
|
|
|
|
if page_types:
|
|
|
|
|
|
return page_types
|
|
|
|
|
|
# 无配置时根据产品类型生成默认序列
|
|
|
|
|
|
products = deck.get("products", [])
|
|
|
|
|
|
kind = products[0].get("kind", "savings") if products else "savings"
|
|
|
|
|
|
return DEFAULT_PAGE_TYPES.get(kind, DEFAULT_PAGE_TYPES["savings"])
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── 主渲染函数 ──────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def render_deck(deck: dict, output_path: str, theme: str = "deepblue") -> dict:
|
|
|
|
|
|
"""渲染 DeckContract 为 PPTX。"""
|
|
|
|
|
|
colors = THEMES.get(theme, THEMES["deepblue"])
|
|
|
|
|
|
|
2026-07-29 21:26:48 +08:00
|
|
|
|
source_template_path = (
|
|
|
|
|
|
deck.get("templateConfig", {}).get("sourceTemplatePath")
|
|
|
|
|
|
)
|
|
|
|
|
|
if source_template_path and os.path.isfile(source_template_path):
|
|
|
|
|
|
prs = Presentation(source_template_path)
|
|
|
|
|
|
while len(prs.slides):
|
|
|
|
|
|
slide_id = prs.slides._sldIdLst[0]
|
|
|
|
|
|
prs.part.drop_rel(slide_id.rId)
|
|
|
|
|
|
del prs.slides._sldIdLst[0]
|
|
|
|
|
|
else:
|
|
|
|
|
|
prs = Presentation()
|
2026-07-23 13:10:50 +08:00
|
|
|
|
prs.slide_width = Inches(13.33)
|
|
|
|
|
|
prs.slide_height = Inches(7.5)
|
|
|
|
|
|
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
template_config = deck.get("templateConfig", {})
|
2026-07-29 17:24:34 +08:00
|
|
|
|
slides_config = deck.get("scenarioSlides") or template_config.get("slidesConfig", [])
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
page_types = _resolve_page_types(deck)
|
|
|
|
|
|
|
2026-07-29 17:24:34 +08:00
|
|
|
|
slide_errors = []
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
for i, page_type in enumerate(page_types):
|
|
|
|
|
|
builder = SLIDE_BUILDERS.get(page_type)
|
|
|
|
|
|
if not builder:
|
|
|
|
|
|
continue
|
|
|
|
|
|
meta = _slide_meta(slides_config, i, page_type)
|
|
|
|
|
|
try:
|
|
|
|
|
|
builder(prs, deck, colors, meta)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[warn] slide {i} ({page_type}) failed: {e}", file=sys.stderr)
|
2026-07-29 17:24:34 +08:00
|
|
|
|
slide_errors.append(f"{page_type}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
if deck.get("scenarioSlides") and slide_errors:
|
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
|
"required scenario slides failed: " + "; ".join(slide_errors)
|
|
|
|
|
|
)
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
|
|
|
|
|
prs.save(output_path)
|
|
|
|
|
|
file_size = os.path.getsize(output_path)
|
|
|
|
|
|
slide_count = len(prs.slides)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"ok": True,
|
|
|
|
|
|
"path": output_path,
|
|
|
|
|
|
"size": file_size,
|
|
|
|
|
|
"slides": slide_count,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── CLI 入口 ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
|
parser = argparse.ArgumentParser(description="Fast PPTX Renderer")
|
|
|
|
|
|
parser.add_argument("--deck-json", required=True, help="DeckContract JSON 文件路径")
|
|
|
|
|
|
parser.add_argument("--output", required=True, help="输出 PPTX 文件路径")
|
feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.
Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
simple_return, compound_return, find_payback_year
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00
|
|
|
|
parser.add_argument("--theme", default="deepblue",
|
2026-07-29 12:19:26 +08:00
|
|
|
|
choices=["deepblue", "caramel", "chinese", "business", "minimal", "ink"],
|
|
|
|
|
|
help="主题配色")
|
2026-07-23 13:10:50 +08:00
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(args.deck_json, "r", encoding="utf-8") as f:
|
|
|
|
|
|
deck = json.load(f)
|
|
|
|
|
|
result = render_deck(deck, args.output, args.theme)
|
|
|
|
|
|
print(json.dumps(result, ensure_ascii=False))
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(json.dumps({"ok": False, "error": str(e)}, ensure_ascii=False))
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main()
|