End-to-end automated generation of the Xingyin Wealth Management Multi-Asset Investment Department product operations weekly report. The full process includes: (1) updating the config date settings, (2) running the Python data-processing pipeline (moving files, AI product classification, generating result tables, calculating benchmarks, performance forecasting), (3) using openpyxl to update external file paths and formula references in Excel linked workbooks, and (4) taking screenshots of Excel sheets and inserting them into a Word template in bookmark order to produce the final docx/pdf weekly report. This is triggered when the user says "generate the weekly report for XX date", "generate this week's report", "weekly report automation", or "run weekly report for YYYYMMDD".
brew install --cask libreoffice (PDF生成需要)~/Downloads 或对应目录步骤1: 更新config日期 → 步骤2: Python数据处理管线(5个脚本)
→ 步骤3: 更新Excel链接工作簿 → 步骤4: Excel表格截图(含VLOOKUP解析)
→ 步骤5: 组装Word文档 + 生成PDF
从用户输入中提取报告日期 (格式: YYYYMMDD)。
更新 config.py:
# /Users/fanshengxia/Desktop/周报V2/config.py 中第46行左右
YUNZUOGAILAN_DATE = 'YYYYMMDD' # 改为用户指定的日期
其他日期 (LASTW, LASTM, LASTY, WEEK_MON 等) 由 Config 类从此日期自动计算,无需手动改。
验证: 改完后用 Python 快速验证日期计算:
cd /Users/fanshengxia/Desktop/周报V2 && python -c "from config import Config; print(f'报告日期:{Config.YUNZUOGAILAN_DATE}, 上周:{Config.LASTW}, 上月:{Config.LASTM}')"
按顺序执行5个脚本。可使用主控脚本一键运行,也可逐个执行:
cd /Users/fanshengxia/Desktop/周报V2
echo "y" | python run_weekly_report.py
cd /Users/fanshengxia/Desktop/周报V2
# 2.1 移动数据源文件到正确目录
python move_files.py
# 2.2 AI产品分类 (新产品自动归类)
python update_new_product_catgoery.py
# 2.3 生成主结果表 + AI文字总结 (最核心的步骤,约5-15分钟)
python generate_report.py
# 2.4 计算业绩基准表现 (需要Wind API)
python calculate_all_benchmarks.py --date {ZHOUBAO_DATE}
# 2.5 收益预测 (合享/悦动稳享系列)
python yield_forecast.py
输出文件: 输出结果/输出结果{DATE}.xlsx (含40+个sheet)
验证: 确认输出文件存在且包含关键sheet:
python -c "
import openpyxl
wb = openpyxl.load_workbook('/Users/fanshengxia/Desktop/周报V2/输出结果/输出结果{DATE}.xlsx', read_only=True)
print(f'Sheet数量: {len(wb.sheetnames)}')
print('关键Sheet:', [s for s in wb.sheetnames if '总结文字' in s or '资产大表' in s or '分渠道' in s])
wb.close()
"
目标: 替代原来的VBA宏操作。用openpyxl直接更新 周报生成工具_加权_调整兴动合兴系列.xlsm 中所有外部文件引用路径。
核心原理: 工作簿中68个sheet通过VLOOKUP公式引用外部Excel文件。需要把公式中的旧日期替换为新日期。
import openpyxl
WORKBOOK_PATH = '/Users/fanshengxia/Desktop/周报V2/周报生成工具_加权_调整兴动合兴系列.xlsm'
wb = openpyxl.load_workbook(WORKBOOK_PATH, keep_vba=True)
# 从 链接数据表 sheet 读取旧路径(B列)和新路径(C列)
link_sheet = wb['链接数据表']
for row in link_sheet.iter_rows(min_row=2, max_col=3, values_only=False):
old_path = row[1].value # B列:旧文件路径
new_path = row[2].value # C列:新文件路径
if old_path and new_path:
# 更新B列为C列的值
row[1].value = new_path
遍历所有数据sheet,将公式中的旧日期替换为新日期:
OLD_DATE = '20260129' # 上一期日期 (从链接数据表旧路径中提取)
NEW_DATE = '20260205' # 本期日期
for sheet_name in wb.sheetnames:
ws = wb[sheet_name]
for row in ws.iter_rows():
for cell in row:
if isinstance(cell.value, str) and OLD_DATE in cell.value:
cell.value = cell.value.replace(OLD_DATE, NEW_DATE)
ws_flow = wb['周报生成流程']
# 更新B2 (当前日期) 和 B3 (上期日期)
ws_flow['B2'] = NEW_DATE
ws_flow['B3'] = OLD_DATE
保存时必须 keep_vba=True 以保留.xlsm中的VBA代码:
wb.save(WORKBOOK_PATH)
核心脚本: /tmp/generate_excel_screenshots_v2.py (~800行)
该脚本从VBA工作簿读取59个数据sheet,生成高保真HTML(保留Excel原始格式、合并单元格、颜色样式),解析VLOOKUP/IF/SUM等公式从外部文件获取数据,最后通过Playwright截图为PNG。
python /tmp/generate_excel_screenshots_v2.py
重要: [1], [2]等编号对应xlsm zip中 xl/externalLinks/ 的顺序,不是链接数据表行号:
EXTERNAL_FILES = {
1: '数据/合享承诺收益率.xlsx', # [1] = 合享承诺收益率 (静态文件)
2: f'输出结果/输出结果{DATE}.xlsx', # [2] = Python输出结果
3: f'数据/产品运作概览数据-母子产品/产品运作概览信息表增加指标变化_{DATE}.xlsx', # [3] = 运作概览
4: f'数据/产品业绩指标数据/内部产品业绩_{DATE}.xlsx', # [4] = 产品业绩
5: f'数据/收益测算数据/丰利合享底表-{DATE}.xlsx', # [5] = 合享底表
6: f'数据/收益测算数据/悦动稳享底表-{DATE}.xlsx', # [6] = 悦动稳享底表
}
绝对不要用 read_only=True! 它会创建 EmptyCell 对象,没有 .row/.column 属性,导致所有外部数据加载静默失败:
# 错误 - EmptyCell没有row/column属性会导致崩溃
wb = openpyxl.load_workbook(path, read_only=True, data_only=True)
# 正确
wb = openpyxl.load_workbook(path, data_only=True)
支持的公式模式(按频率排序):
IF($A2="","",VLOOKUP(...)) — 14,536个VLOOKUP(lookup, '[2]Sheet'!$A:$Z, col, 0) — 8,040个 (含全列引用)IF([2]Sheet!$A4="","",[2]Sheet!$A4) — 3,744个 (外部单元格引用)SUM(B3:B10) — 1,507个IFERROR(VALUE(VLOOKUP(...)),fallback) — 534个+,-,*,/ — 212个多轮解析: 公式之间有依赖关系(SUM依赖VLOOKUP结果),需要5轮迭代解析。
输出:
/tmp/weekly_report_html/ (59个)/tmp/weekly_report_screenshots/ (59个)部分sheet因公式复杂度限制,数据填充不完整(属正常现象):
核心脚本: /tmp/assemble_word_report.py (~500行)
python-docx 打开 .docm 会报 ValueError: content type is 'application/vnd.ms-word.document.macroEnabled.main+xml'。
解决方案: 用 zipfile 方式将 .docm 转换为 .docx:
import zipfile, re
def convert_docm_to_docx(docm_path, docx_path):
"""Strip VBA from .docm to create valid .docx"""
vba_files = {'word/vbaProject.bin', 'word/vbaData.xml', 'word/_rels/vbaProject.bin.rels'}
with zipfile.ZipFile(docm_path, 'r') as zin:
with zipfile.ZipFile(docx_path, 'w', zipfile.ZIP_DEFLATED) as zout:
for item in zin.namelist():
if item in vba_files:
continue
data = zin.read(item)
if item == '[Content_Types].xml':
content = data.decode('utf-8')
content = content.replace(
'application/vnd.ms-word.document.macroEnabled.main+xml',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml')
content = re.sub(r'<Override[^>]*vbaData[^>]*/>', '', content)
content = re.sub(r'<Default[^>]*Extension="bin"[^>]*/>', '', content)
data = content.encode('utf-8')
elif item == 'word/_rels/document.xml.rels':
content = data.decode('utf-8')
content = re.sub(r'<Relationship[^>]*vbaProject[^>]*/>', '', content)
data = content.encode('utf-8')
zout.writestr(item, data)
所有66个书签都是空书签(bookmarkStart和bookmarkEnd相邻),需要在两者之间插入内容。
文字书签插入:
from lxml import etree
W = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
# 构建 <w:r><w:t>text</w:t></w:r> 元素
run_xml = f'<w:r xmlns:w="..."><w:rPr><w:rFonts w:hint="eastAsia"/><w:sz w:val="21"/></w:rPr><w:t xml:space="preserve">{text}</w:t></w:r>'
run_elem = etree.fromstring(run_xml.encode())
bm_start.addnext(run_elem)
图片书签插入:
# 1. 将PNG文件添加到docx zip的 word/media/ 目录
# 2. 在 word/_rels/document.xml.rels 添加 Relationship
# 3. 构建 <w:r><w:drawing><wp:inline>...</wp:inline></w:drawing></w:r> XML
# 4. 插入到 bookmarkStart 后面
# 图片尺寸: 按96 DPI计算EMU (1 EMU = 1/914400 inch)
emu_per_px = 914400 / 96 # = 9525
cx = int(px_width * emu_per_px)
cy = int(px_height * emu_per_px)
# 缩放到页面宽度 (A4: 11906 twips - 720*2 margins = 10466 twips content)
MAX_WIDTH_EMU = 10466 * 635 # ~6,645,910 EMU
if cx > MAX_WIDTH_EMU:
scale = MAX_WIDTH_EMU / cx
cx = MAX_WIDTH_EMU
cy = int(cy * scale)
多行文字处理: 规模文字、中收文字、到期文字含换行符。第一行插入当前段落,后续行创建新段落(复制原段落样式但排除sectPr)。
| 书签名 | 内容来源 | 说明 |
|--------|----------|------|
| 首页日期 | 固定格式 | 2026年02月05日 |
| 到期日期 | 固定格式 | 2026年02月05日 |
| 规模文字 | 输出结果.xlsx → 总结文字 sheet B4 | AI生成的规模总结 |
| 中收文字 | 输出结果.xlsx → 总结文字 sheet B3 | AI生成的中收总结 |
| 到期文字 | 输出结果.xlsx → 总结文字 sheet B2 | AI生成的到期总结 |
| 市场文字 | 留空 | 通常需手动填写 |
| 目标盈 | 留空 | 通常需手动填写 |
必须在 [Content_Types].xml 中添加PNG的Default:
<Default Extension="png" ContentType="image/png"/>
# LibreOffice headless (推荐,最可靠)
soffice --headless --convert-to pdf --outdir "输出结果/" "输出结果/多资产投资部产品周报-{DATE}.docx"
注意: Word for Mac 的 AppleScript save as 不工作(返回 -1708 错误)。JXA exportAs 可以执行但不一定生成文件。LibreOffice headless 是最可靠的方案。
python /tmp/assemble_word_report.py
输出:
输出结果/多资产投资部产品周报-{DATE}.docx (~13MB)输出结果/多资产投资部产品周报-{DATE}.pdf (~12MB, ~57页)| 路径 | 说明 |
|------|------|
| /Users/fanshengxia/Desktop/周报V2/ | 项目根目录 |
| config.py | 配置文件 (日期、参数) |
| run_weekly_report.py | Python主控脚本 |
| 周报生成工具_加权_调整兴动合兴系列.xlsm | Excel VBA工作簿 (68 sheets) |
| 多资产投资部产品周报-模版.docm | Word模版 (66个书签) |
| 输出结果/ | 所有输出文件目录 |
| 数据/ | 所有源数据目录 |
| /tmp/generate_excel_screenshots_v2.py | 步骤4截图脚本 (~800行) |
| /tmp/assemble_word_report.py | 步骤5组装脚本 (~500行) |
| 文件 | 说明 |
|------|------|
| 输出结果/输出结果{DATE}.xlsx | Python输出 (40+ sheets) |
| 数据/产品运作概览数据-母子产品/产品运作概览信息表增加指标变化_{DATE}.xlsx | 产品运作概览 |
| 数据/产品业绩指标数据/内部产品业绩_{DATE}.xlsx | 业绩指标 |
| 数据/收益测算数据/丰利合享底表-{DATE}.xlsx | 合享收益预测 |
| 数据/收益测算数据/悦动稳享底表-{DATE}.xlsx | 悦动稳享收益预测 |
| 数据/合享承诺收益率.xlsx | 合享承诺收益率 (静态文件) |
| 文件 | 格式 | 大小 |
|------|------|------|
| 输出结果/多资产投资部产品周报-{DATE}.docx | Word文档 | ~13MB |
| 输出结果/多资产投资部产品周报-{DATE}.pdf | PDF文档 (~57页) | ~12MB |
| 错误 | 解决方案 |
|------|----------|
| Wind未连接 | 跳过步骤2.4(业绩基准),后续手动补充 |
| config日期格式错误 | 必须为YYYYMMDD格式,如20260205 |
| 数据文件缺失 | 检查Downloads中文件是否已下载,运行move_files.py |
| NAV缓存异常 | python manage_nav_cache.py clear 清除缓存后重试 |
| AI摘要生成失败 | 检查DeepSeek API key和网络连接 |
| openpyxl公式替换失效 | 确认旧日期字符串正确匹配 |
| openpyxl read_only=True崩溃 | 绝对不要用read_only=True,EmptyCell没有row/column属性 |
| python-docx打不开.docm | 用zipfile方式剥离VBA后转为.docx再处理 |
| Word AppleScript save as失败 | 用LibreOffice headless: soffice --headless --convert-to pdf |
| LibreOffice未安装 | brew install --cask libreoffice |
| 截图公式数据缺失 | 部分复杂公式(SUMPRODUCT等)无法解析,属正常现象 |
references/bookmark_mapping.md输出结果/多资产投资部产品周报-{LAST_DATE}.pdf用户: "生成20260205的周报"
执行:
YUNZUOGAILAN_DATE = '20260205'run_weekly_report.py (5个步骤)/tmp/generate_excel_screenshots_v2.py 截图59个表格/tmp/assemble_word_report.py 组装Word (文字+图片→书签)soffice --headless --convert-to pdf 导出PDF报告:
已生成 20260205 周报
输出文件:
- DOCX: 输出结果/多资产投资部产品周报-20260205.docx (13MB)
- PDF: 输出结果/多资产投资部产品周报-20260205.pdf (12MB, 57页)
执行摘要:
- Python管线: 5/5 步骤成功
- Excel链接更新: 68 sheets, 6个外部文件引用已更新
- 表格截图: 59/59 完成 (含VLOOKUP公式解析)
- Word书签: 64/66 已填充 (市场文字和目标盈留空)
- AI文字总结: 规模/中收/到期 3段已生成
Google Workspace CLI for Gmail, Calendar, Drive, Contacts, Sheets, and Docs.
Manage Apple Notes via the `memo` CLI on macOS (create, view, edit, delete, search, move, and export notes). Use when a user asks OpenClaw to add a note, list notes, search notes, or manage note folders.
Work with Obsidian vaults (plain Markdown notes) and automate via obsidian-cli.
Use when you need to control Slack from OpenClaw via the slack tool, including reacting to messages or pinning/unpinning items in Slack channels or DMs.
Manage Apple Reminders via remindctl CLI (list, add, edit, complete, delete). Supports lists, date filters, and JSON/plain output.
Manage Trello boards, lists, and cards via the Trello REST API.
Category:productivity