Skill: richard052317/cost-report-generator (version 1.0.0)

--- name: cost-report-generator description: 从项目费用 Excel 文件（如 EVP 项目费用.xlsx）自动生成按客户分类的成本分析报告。支持帝国、BAT、MT、ITMS 等客户分类，包含 2025 年和 2026 年成本分析，自动计算年度汇总、月度明细、月均数据。使用场景：用户发送类似"EVP 项目费用.xlsx"的文件并要求生成成本分析报告，或提到"按客户提炼成本"、"生成成本分析报告"、"整理项目费用"等。 --- # 成本分析报告生成器 从项目费用 Excel 文件自动生成按客户分类的成本分析报告。 ## 触发条件 当用户： - 上传类似 `EVP 项目费用.xlsx` 的文件并要求生成成本分析报告 - 提到"按客户分类生成成本报告"、"提炼成本分析"、"整理项目费用" - 发送包含客户费用数据的 Excel 文件 ## 核心功能 1. **自动识别客户分类**：帝国、BAT、MT、ITMS 等 2. **费用分类汇总**： - HR 费用（人工费用） - 试产/打样费用（打样 + 制造 + 物料耗用） - 测试费用（测试 + 外发认证） - 开发投资（开模 + 改模） - 其他费用（IDC、品烟、其它、手板） 3. **时间维度**：2025 年（12 个月）、2026 年（1-2 月） 4. **数据指标**：年度汇总、月均、月度明细 5. **格式化输出**：千分符、两位小数、美化样式 ## 使用方法 ### 方式 1：直接运行脚本 ```bash python3 ~/.openclaw/workspace/skills/cost-report-generator/scripts/generate_cost_report.py <源文件路径> [输出目录] ``` 示例： ```bash python3 ~/.openclaw/workspace/skills/cost-report-generator/scripts/generate_cost_report.py ~/Desktop/EVP_项目费用.xlsx ``` ### 方式 2：通过 Python 调用 ```python from scripts.generate_cost_report import generate_cost_report output_file = generate_cost_report( source_file="/path/to/EVP_项目费用.xlsx", output_dir="/path/to/output" # 可选，默认为源文件所在目录 ) ``` ## 输入文件格式 ### 必需列 - **客户大类**：客户分类（帝国、BAT、MT、ITMS 等） - **项目大类**：项目名称/类别 - **内部订单号**：订单编号 ### 费用列（按年份和月份） 支持的费用类型： - 人工费用 - 打样费用 - 制造费用 - 物料耗用 - 测试费用 - 外发认证费用 - 开模费用 - 改模费用 - IDC 费用 - 品烟费用 - 其它费用 - 手板费用 列名格式：`{年份} 年{月份} 月{费用类型}`，如： - `25 年 1 月人工费用` - `25 年打样费用`（年度汇总） - `26 年 1-2 月人工费用` ### 表头位置 脚本默认使用**第 4 行**（索引 3）作为表头。如果源文件格式不同，需要调整 `header` 参数。 ## 输出报告结构 ### 工作表 1. **2025 年成本分析**：按客户分类展示 2025 年 1-12 月数据 2. **2026 年成本分析**：按客户分类展示 2026 年 1-2 月数据 ### 列结构（每个客户分类） | 列范围 | 内容 | |--------|------| | A-C | 基本信息（客户大类、项目大类、内部订单号） | | D-E | HR 费用（汇总、月均） | | F-R | HR 费用月度明细（1-12 月） | | S-T | 试产/打样费用（汇总、月均） | | U-AG | 试产/打样费用月度明细 | | ... | 其他费用类别同理 | | 最后两列 | 总计（汇总、月度合计） | ### 样式特点（必须严格遵守） - **表头**：深蓝色背景 (#2E74B5) + 白色文字，微软雅黑字体 - **子标题**：浅蓝色背景 (#D6EAF8) + 深色文字 - **数据行**：交替使用白色/浅灰色背景 (#F8F9FA)，便于阅读 - **总计列**：黄色高亮 (#FFEB9C) + 粗体，突出显示 - **月度合计**：绿色高亮 (#C6EFCE) + 粗体 - **数字格式**：`#,##0.00`（千分符 + 两位小数），所有数值统一 - **对齐方式**：文本左对齐，数字右对齐 - **列宽**：基本信息列加宽（14-20），月度列统一（10），总计列加宽（14） - **边框**：细边框 + 浅灰色 (#CCCCCC) ## 月均计算逻辑 - **2025 年**：年度汇总 ÷ 12 - **2026 年**：年度汇总 ÷ 2（因为只有 1-2 月数据） 脚本会自动统计实际有数据的月份数，并按以下规则计算： ```python max_months = 2 if year == 2026 else 12 divisor = min(actual_months_count, max_months) monthly_avg = monthly_sum / divisor ``` ## 注意事项 1. **表头位置**：确保源文件的表头在第 4 行（索引 3），否则需要调整 `header` 参数 2. **客户分类**：只处理"帝国"、"BAT"、"MT"、"ITMS"四类，其他会被忽略 3. **数据完整性**：如果某些月份数据缺失，月均计算会自动调整除数 4. **输出位置**：默认保存到源文件所在目录，可通过 `output_dir` 参数指定 ## 故障排除 ### 问题：读取数据为空 **原因**：表头位置不正确 **解决**：检查源文件的表头位置，修改脚本中的 `header` 参数： ```python df = pd.read_excel(source_file, sheet_name='Sheet1', header=3) # 调整为正确的行号 ``` ### 问题：费用数据为 0 **原因**：列名格式不匹配 **解决**：检查源文件的列名格式，确保包含"年"、"月"等关键字，如`25 年 1 月人工费用` ### 问题：客户分类缺失 **原因**：客户大类列的值与预期不符 **解决**：检查"客户大类"列的实际值，必要时在脚本的 `valid_customers` 列表中添加新的客户类型 ## 相关文件 - **脚本**：`scripts/generate_cost_report.py` - 核心生成脚本 ## 分享与发布 ### 本地使用（当前用户） 技能已安装在 `~/.openclaw/workspace/skills/cost-report-generator/`，当前 OpenClaw 会话可直接使用。 ### 分享给其他飞书用户 **方式 1：通过 ClawHub 发布（推荐）** ```bash # 1. 登录 ClawHub clawhub login # 2. 发布技能 clawhub publish ~/.openclaw/workspace/skills/cost-report-generator \ --slug cost-report-generator \ --name "成本分析报告生成器" \ --version 1.0.0 \ --changelog "初始版本：支持 EVP 项目费用文件自动生成成本分析报告" ``` 其他用户安装： ```bash clawhub install cost-report-generator ``` **方式 2：直接共享技能文件夹** 将 `~/.openclaw/workspace/skills/cost-report-generator/` 打包发送给其他用户，让他们放到自己的 `~/.openclaw/workspace/skills/` 目录下。 **方式 3：通过 OpenClaw 团队空间** 如果多个用户共享同一个 OpenClaw workspace，技能会自动对所有用户可见。 ### 技能元数据（发布用） ```yaml name: cost-report-generator version: 1.0.0 description: 从项目费用 Excel 自动生成按客户分类的成本分析报告 author: Richard 张 tags: [成本分析，财务报告，Excel, 飞书] ```

path: scripts/generate_cost_report.py
content: import pandas as pd
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from openpyxl.utils import get_column_letter
import sys
import os

def generate_cost_report(source_file, output_dir=None):
    """
    从项目费用 Excel 文件生成成本分析报告
    
    Args:
        source_file: 源 Excel 文件路径（如 EVP 项目费用.xlsx）
        output_dir: 输出目录，默认为源文件所在目录
    
    Returns:
        输出文件路径
    """
    # 读取源文件（第 4 行作为表头，索引从 1 开始）
    # 尝试读取第一个工作表，表头在第 2 行（索引 1）
    # HP 项目文件格式：表头在第 2 行（索引 1）
    df = pd.read_excel(source_file, sheet_name=0, header=1)
    print(f"读取数据：{len(df)}行，{len(df.columns)}列")
    
    # HP 项目文件：第 0 行是数据，不需要跳过
    # 提取基本信息
    customers = [str(df.iloc[i]['客户大类']) if pd.notna(df.iloc[i]['客户大类']) else '' for i in range(len(df))]
    projects = [str(df.iloc[i]['项目大类']) if pd.notna(df.iloc[i]['项目大类']) else '' for i in range(len(df))]
    order_ids = [str(df.iloc[i]['内部订单号']) if pd.notna(df.iloc[i]['内部订单号']) else '' for i in range(len(df))]
    
    # 筛选有效客户分类
    valid_customers = ['帝国', 'BAT', 'MT', 'ITMS']
    valid_indices = [i for i in range(len(df)) if customers[i] in valid_customers]
    
    # 按客户分类分组
    customer_groups = {}
    for cust_type in valid_customers:
        customer_groups[cust_type] = [i for i in valid_indices if customers[i] == cust_type]
    
    print(f"客户分布：{ {k: len(v) for k, v in customer_groups.items()} }")
    
    # 列分类
    def find_cols(keyword):
        return [col for col in df.columns if keyword in str(col)]
    
    hr_cols = find_cols('人工费用')
    sample_cols = find_cols('打样费用')
    manufacturing_cols = find_cols('制造费用')
    material_cols = find_cols('物料耗用')
    test_cols = find_cols('测试费用')
    cert_cols = find_cols('外发认证费用')
    mold_cols = find_cols('开模费用')
    modify_cols = find_cols('改模费用')
    other_cols = find_cols('IDC 费用') + find_cols('品烟费用') + find_cols('其它费用') + find_cols('手板费用')
    
    def extract_month(col_str):
        if '1-' in col_str:
            return '1-2 月'
        if '年' in col_str and '月' in col_str:
            year_pos = col_str.find('年')
            month_pos = col_str.find('月')
            if year_pos >= 0 and month_pos > year_pos:
                between = col_str[year_pos+1:month_pos]
                if between.isdigit():
                    return f"{between} 月"
        return None
    
    def get_data(row_idx, cat_cols, year):
        year_short = str(year)[-2:]
        months_order = ['1-2 月', '1 月', '2 月', '3 月', '4 月', '5 月', '6 月', '7 月', '8 月', '9 月', '10 月', '11 月', '12 月'] if year == 2026 else ['1 月', '2 月', '3 月', '4 月', '5 月', '6 月', '7 月', '8 月', '9 月', '10 月', '11 月', '12 月']
        
        yearly_total = 0
        monthly_data = {m: 0.0 for m in months_order}
        actual_months_count = 0
        
        for col in cat_cols:
            col_str = str(col)
            if year_short not in col_str or '年' not in col_str:
                continue
            val = df.iloc[row_idx][col]
            try:
                num = float(val) if pd.notna(val) else 0.0
            except:
                num = 0.0
            
            if '月' not in col_str and '1-' not in col_str:
                yearly_total += num
            else:
                month = extract_month(col_str)
                if month and month in monthly_data:
                    monthly_data[month] += num
                    if num != 0:
                        actual_months_count += 1
        
        if yearly_total == 0:
            yearly_total = sum(v for v in monthly_data.values())
        
        monthly_sum = sum(v for v in monthly_data.values())
        max_months = 2 if year == 2026 else 12
        divisor = min(actual_months_count, max_months) if actual_months_count > 0 else max_months
        monthly_avg = monthly_sum / divisor if divisor > 0 else 0
        
        return round(yearly_total, 2), {m: (round(v, 2) if v != 0 else None) for m, v in monthly_data.items()}, round(monthly_avg, 2)
    
    # 创建工作簿
    wb = Workbook()
    del wb['Sheet']
    
    # 样式定义
    header_font = Font(name='微软雅黑', bold=True, size=10, color='FFFFFF')
    subheader_font = Font(name='微软雅黑', bold=True, size=9, color='333333')
    normal_font = Font(name='微软雅黑', size=9)
    bold_font = Font(name='微软雅黑', bold=True, size=9)
    header_fill_blue = PatternFill(start_color="2E74B5", end_color="2E74B5", fill_type="solid")
    header_fill_light = PatternFill(start_color="D6EAF8", end_color="D6EAF8", fill_type="solid")
    alt_fill = PatternFill(start_color="F8F9FA", end_color="F8F9FA", fill_type="solid")
    center_align = Alignment(horizontal='center', vertical='center')
    left_align = Alignment(horizontal='left', vertical='center')
    right_align = Alignment(horizontal='right', vertical='center')
    num_format = '#,##0.00'
    thin_border = Border(left=Side(style='thin', color='CCCCCC'), right=Side(style='thin', color='CCCCCC'), top=Side(style='thin', color='CCCCCC'), bottom=Side(style='thin', color='CCCCCC'))
    
    def create_sheet(name, year):
        ws = wb.create_sheet(name)
        months = ['1-2 月', '1 月', '2 月', '3 月', '4 月', '5 月', '6 月', '7 月', '8 月', '9 月', '10 月', '11 月', '12 月'] if year == 2026 else ['1 月', '2 月', '3 月', '4 月', '5 月', '6 月', '7 月', '8 月', '9 月', '10 月', '11 月', '12 月']
        categories = ['HR 费用', '试产/打样费用', '测试费用', '开发投资', '其他费用']
        num_months = len(months)
        
        col = 1
        ws.merge_cells(start_row=1, start_column=1, end_column=3, end_row=1)
        cell = ws.cell(row=1, column=1, value="基本信息")
        cell.font, cell.fill, cell.alignment, cell.border = header_font, header_fill_blue, center_align, thin_border
        
        col = 4
        for cat in categories:
            end_col = col + num_months + 1
            ws.merge_cells(start_row=1, start_column=col, end_column=end_col, end_row=1)
            cell = ws.cell(row=1, column=col, value=cat)
            cell.font, cell.fill, cell.alignment, cell.border = header_font, header_fill_blue, center_align, thin_border
            col = end_col + 1
        
        ws.merge_cells(start_row=1, start_column=col, end_column=col+1, end_row=1)
        cell = ws.cell(row=1, column=col, value="总计")
        cell.font, cell.fill, cell.alignment, cell.border = header_font, header_fill_blue, center_align, thin_border
        
        for c, val in enumerate(["客户大类", "项目大类", "内部订单号"], 1):
            cell = ws.cell(row=2, column=c, value=val)
            cell.font = subheader_font
            cell.fill = header_fill_light
            cell.alignment = center_align
            cell.border = thin_border
        
        col = 4
        for cat in categories:
            cell = ws.cell(row=2, column=col, value="汇总")
            cell.font = subheader_font
            cell.fill = header_fill_light
            cell.alignment = center_align
            cell.border = thin_border
            col += 1
            cell = ws.cell(row=2, column=col, value="月均")
            cell.font = subheader_font
            cell.fill = header_fill_light
            cell.alignment = center_align
            cell.border = thin_border
            col += 1
            for month in months:
                cell = ws.cell(row=2, column=col, value=month)
                cell.font = normal_font
                cell.fill = header_fill_light
                cell.alignment = center_align
                cell.border = thin_border
                col += 1
        
        ws.cell(row=2, column=col, value="汇总").font = subheader_font
        ws.cell(row=2, column=col).fill = header_fill_light
        ws.cell(row=2, column=col).alignment = center_align
        ws.cell(row=2, column=col).border = thin_border
        col += 1
        ws.cell(row=2, column=col, value="月度合计").font = subheader_font
        ws.cell(row=2, column=col).fill = header_fill_light
        ws.cell(row=2, column=col).alignment = center_align
        ws.cell(row=2, column=col).border = thin_border
        
        return ws, months, thin_border
    
    def add_data(ws, indices, start_row, year, border):
        months = ['1-2 月', '1 月', '2 月', '3 月', '4 月', '5 月', '6 月', '7 月', '8 月', '9 月', '10 月', '11 月', '12 月'] if year == 2026 else ['1 月', '2 月', '3 月', '4 月', '5 月', '6 月', '7 月', '8 月', '9 月', '10 月', '11 月', '12 月']
        cats = [('HR', hr_cols), ('Sample', sample_cols + manufacturing_cols + material_cols), ('Test', test_cols + cert_cols), ('Mold', mold_cols + modify_cols), ('Other', other_cols)]
        
        row = start_row
        use_alt_fill = False
        for idx in indices:
            if customers[idx] == '':
                continue
            row_fill = alt_fill if use_alt_fill else None
            
            for c, val in enumerate([customers[idx], projects[idx], order_ids[idx]], 1):
                cell = ws.cell(row=row, column=c, value=val)
                cell.border = border
                cell.alignment = left_align
                cell.font = normal_font
                if row_fill:
                    cell.fill = row_fill
            
            col = 4
            grand_total = 0.0
            monthly_sum = 0.0
            
            for _, cat_cols in cats:
                yearly, monthly, monthly_avg = get_data(idx, cat_cols, year)
                cell = ws.cell(row=row, column=col, value=yearly if yearly != 0 else None)
                cell.border = border
                cell.alignment = right_align
                cell.font = bold_font if yearly != 0 else normal_font
                cell.number_format = num_format
                if row_fill:
                    cell.fill = row_fill
                col += 1
                
                cell = ws.cell(row=row, column=col, value=monthly_avg if monthly_avg != 0 else None)
                cell.border = border
                cell.alignment = right_align
                cell.font = normal_font
                cell.number_format = num_format
                if row_fill:
                    cell.fill = row_fill
                col += 1
                
                for m in months:
                    v = monthly.get(m)
                    cell = ws.cell(row=row, column=col, value=v)
                    cell.border = border
                    cell.alignment = right_align
                    cell.font = normal_font
                    cell.number_format = num_format
                    if row_fill:
                        cell.fill = row_fill
                    if v is not None:
                        monthly_sum += v
                    col += 1
                
                grand_total += yearly
            
            cell = ws.cell(row=row, column=col, value=round(grand_total, 2))
            cell.font = bold_font
            cell.fill = PatternFill(start_color="FFEB9C", end_color="FFEB9C", fill_type="solid")
            cell.alignment = right_align
            cell.border = border
            cell.number_format = num_format
            col += 1
            
            cell = ws.cell(row=row, column=col, value=round(monthly_sum, 2))
            cell.font = bold_font
            cell.fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid")
            cell.alignment = right_align
            cell.border = border
            cell.number_format = num_format
            
            row += 1
            use_alt_fill = not use_alt_fill
        return row
    
    # 生成 2025 年工作表
    print("生成 2025 年成本分析...")
    ws25, _, border = create_sheet("2025 年成本分析", 2025)
    row = 3
    for cust_type in valid_customers:
        indices = customer_groups[cust_type]
        if indices:
            ws25.cell(row=row-1, column=1, value=f"【{cust_type}客户】").font = Font(name='微软雅黑', bold=True, size=11, color='2E74B5')
            row = add_data(ws25, indices, row, 2025, border)
            row += 1
    
    # 生成 2026 年工作表
    print("生成 2026 年成本分析...")
    ws26, _, border = create_sheet("2026 年成本分析", 2026)
    row = 3
    for cust_type in valid_customers:
        indices = customer_groups[cust_type]
        if indices:
            ws26.cell(row=row-1, column=1, value=f"【{cust_type}客户】").font = Font(name='微软雅黑', bold=True, size=11, color='2E74B5')
            row = add_data(ws26, indices, row, 2026, border)
            row += 1
    
    # 设置列宽
    for ws in [ws25, ws26]:
        ws.column_dimensions['A'].width = 14
        ws.column_dimensions['B'].width = 20
        ws.column_dimensions['C'].width = 16
        ws.column_dimensions['D'].width = 14
        ws.column_dimensions['E'].width = 12
        for i in range(6, 72):
            col_letter = get_column_letter(i)
            if col_letter not in ['D', 'E']:
                ws.column_dimensions[col_letter].width = 10
        ws.column_dimensions['BT'].width = 14
    
    # 确定输出路径
    if output_dir is None:
        output_dir = os.path.dirname(source_file)
    
    timestamp = pd.Timestamp.now().strftime('%Y%m%d_%H%M')
    source_basename = os.path.basename(source_file).replace('.xlsx', '')
    output_file = os.path.join(output_dir, f"{source_basename}_成本分析报告_{timestamp}.xlsx")
    
    wb.save(output_file)
    print(f"✅ 报告已生成：{output_file}")
    
    return output_file

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("用法：python generate_cost_report.py <源 Excel 文件路径> [输出目录]")
        sys.exit(1)
    
    source_file = sys.argv[1]
    output_dir = sys.argv[2] if len(sys.argv) > 2 else None
    
    generate_cost_report(source_file, output_dir)


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)