lov-expense-report
Extract invoice data from images or text descriptions and generate a categorized Excel expense report. Supports receipt photos, scanned invoices, and manual text input. Auto-classifies into: business entertainment (客户餐费), travel-transport (机票/火车票/打车), travel-accommodation (酒店), t
Install
npx skills add https://github.com/lovstudio/skills/tree/main/skills/expense-report
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lovstudio-skills@llmmart
git clone https://github.com/lovstudio/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lovstudio/skills collection as a plugin from our marketplace. Git is the plain clone.
README
报销管家 · Expense Assistant
Invoice images / text → categorized Excel expense report.
Install
git clone https://example.com/skills/expense-report-skill \
"${SKILL_SKILLS_INSTALL_DIR:?Set SKILL_SKILLS_INSTALL_DIR}/lov-expense-report"
Dependencies
pip install openpyxl
Usage
Invoke in Claude Code:
/lov-expense-report
Then provide invoice images or text descriptions. The skill will:
- Extract invoice details (date, vendor, amount, type)
- Auto-classify into expense categories
- Confirm with you before generating
- Output a styled Excel report with category subtotals
Categories
| Category | Examples |
|---|---|
| 业务招待 | 客户餐费, 商务宴请, 礼品 |
| 差旅-交通 | 机票, 火车票, 打车, 加油 |
| 差旅-住宿 | 酒店, 宾馆 |
| 差旅-餐饮 | 出差工作餐 |
| 办公用品 | 文具, 打印, 办公设备 |
| 通讯费 | 话费, 流量, 宽带 |
| 其他 | 未分类费用 |
Script CLI
python scripts/generate_report.py --input invoices.json --output report.xlsx
Input JSON format:
[
{
"date": "2026-04-15",
"vendor": "海底捞",
"item": "客户餐费",
"amount": 486.0,
"category": "业务招待",
"note": "与XX公司李总晚餐"
}
]
Output
Excel with two sheets:
- 发票报销汇总 — Full itemized report grouped by category
- 分类汇总 — Summary: category, count, subtotal
License
MIT
Skill manifest
报销管家 · Expense Assistant
Extract invoice information from images or text, classify expenses, and generate a professional Excel report with subtotals per category.
When to Use
- User has invoice photos / scanned receipts to process
- User describes expenses in text and wants them organized
- User needs a categorized reimbursement report (报销单)
- User mentions 发票报销, 报销汇总, 发票整理, expense report
Expense Categories
| Category | Keywords / Examples |
|---|---|
| 业务招待 | 客户餐费, 商务宴请, 礼品, 招待 |
| 差旅-交通 | 机票, 火车票, 高铁, 出租车, 打车, 网约车, 加油, 过路费, 停车费 |
| 差旅-住宿 | 酒店, 住宿, 宾馆 |
| 差旅-餐饮 | 出差期间餐费, 工作餐 |
| 办公用品 | 文具, 打印, 办公耗材, 办公设备 |
| 通讯费 | 话费, 流量, 网费, 宽带 |
| 其他 | 不属于以上类别的费用 |
Workflow (MANDATORY)
Step 1: Collect Invoice Data
Accept input in any of these forms:
- Images: Read invoice photos using the Read tool. Extract: date, vendor, amount, item type, notes.
- Text descriptions: Parse the user's text for the same fields.
- Mixed: Multiple images + supplementary text.
For each invoice, extract these fields:
{
"date": "2026-04-15",
"vendor": "海底捞(国贸店)",
"item": "客户餐费",
"amount": 486.00,
"category": "业务招待",
"note": "与XX公司李总晚餐"
}
Step 2: Classify
Assign each invoice to a category from the table above. Rules:
- If the user explicitly states the category, use it.
- If the item/vendor clearly matches a category keyword, auto-assign.
- For ambiguous items (e.g., "餐费" could be 业务招待 or 差旅-餐饮):
- If the note mentions a client/customer → 业务招待
- If the context is a business trip → 差旅-餐饮
- If unclear, ask the user.
Step 3: Confirm with User
Before generating, show the extracted data as a table:
| # | 日期 | 商户 | 项目 | 金额 | 分类 | 备注 |
|---|------|------|------|------|------|------|
| 1 | 2026-04-15 | 海底捞 | 客户餐费 | 486.00 | 业务招待 | 与XX公司李总 |
| 2 | 2026-04-14 | 滴滴出行 | 打车 | 45.50 | 差旅-交通 | 机场→酒店 |
Use AskUserQuestion to ask: "以上信息是否正确? 需要修改或补充吗?"
Step 4: Generate Excel
Write the confirmed data to a temp JSON file, then run:
python expense-report-skill/scripts/generate_report.py \
--input /tmp/invoices.json \
--output "发票报销汇总-YYYYMMDD.xlsx"
JSON format (array of objects):
[
{"date": "2026-04-15", "vendor": "海底捞", "item": "客户餐费", "amount": 486.0, "category": "业务招待", "note": "与XX公司李总"},
{"date": "2026-04-14", "vendor": "滴滴出行", "item": "打车", "amount": 45.5, "category": "差旅-交通", "note": "机场→酒店"}
]
Step 5: Deliver
Tell the user:
- Output file path
- Total amount and breakdown by category
- Remind them to review the "分类汇总" sheet for the summary
Output Format
The Excel file contains two sheets:
- 发票报销汇总 — Full detail, grouped by category with subtotals
- 分类汇总 — Summary table: category, count, subtotal
Style: Skill Publisher warm-academic (terracotta headers #4F46E5, warm cream accents).
Edge Cases
- Blurry/unreadable image: Tell the user which fields couldn't be extracted; ask them to provide manually.
- Foreign currency: Note the currency; convert to CNY if user provides rate, otherwise keep original with note.
- Duplicate invoices: Flag potential duplicates (same date + vendor + amount) before generating.
- No date on invoice: Use the date the user provides, or mark as "日期不详".
Runtime context (shared)
运行前读取本 Skill 包的 skill.yaml,由宿主提供 skill-runtime/v1 上下文。字段解析顺序为:当前请求、项目上下文、个人 Preferences、品牌 Profile、通用默认值。
- 只使用 Manifest 声明的字段;Profile 保存公开品牌事实,Preferences 保存个人工作偏好。
required: true字段缺失时,按 Manifest 的问题配置向用户提出一个聚焦问题;用户明确同意后再保存回答。- 报错提供可复制的
context_id、字段路径与来源,诊断内容避开秘密、完整私人路径和原始配置。
通用反馈闭环
用户在 Skill 驱动任务中提出修改意见时,继续当前产物前必须执行:
- 先判断意见是
task-specific(仅本次)还是reusable(可跨任务复用)。 task-specific只修改当前任务,不改 Skill。reusable先确定作用域:领域规则先更新对应 canonical Skill;适用于所有 Skill 的规则先更新共享规范。- 完成规则更新、版本、lint 与分发核验后,再把修改应用到当前任务。
reusable修改会使此前的“确认”“继续”“发吧”失效;完成当前产物修改和回读后必须停下,等待用户下一步指示,不自动进入发布、提交或其他外部写入。
Files (skills)
-
scripts
-
generate_report.py 7.9 KB
#!/usr/bin/env python3 """Generate categorized expense report Excel from invoice data (JSON input).""" import argparse import json import sys from datetime import datetime from pathlib import Path try: from openpyxl import Workbook from openpyxl.styles import Font, Alignment, PatternFill, Border, Side from openpyxl.utils import get_column_letter except ImportError: print("ERROR: openpyxl required. Install: pip install openpyxl", file=sys.stderr) sys.exit(1) CATEGORIES = { "业务招待": ["客户餐费", "商务宴请", "礼品", "娱乐", "招待"], "差旅-交通": ["机票", "火车票", "高铁", "出租车", "打车", "地铁", "公交", "网约车", "加油", "过路费", "停车费"], "差旅-住宿": ["酒店", "住宿", "宾馆"], "差旅-餐饮": ["出差餐费", "差旅餐饮", "工作餐"], "办公用品": ["文具", "打印", "办公耗材", "办公设备"], "通讯费": ["话费", "流量", "网费", "宽带"], "其他": [], } HEADER_FILL = PatternFill(start_color="CC785C", end_color="CC785C", fill_type="solid") HEADER_FONT = Font(name="Microsoft YaHei", bold=True, color="FFFFFF", size=11) CATEGORY_FILL = PatternFill(start_color="F5E6DE", end_color="F5E6DE", fill_type="solid") CATEGORY_FONT = Font(name="Microsoft YaHei", bold=True, size=11) BODY_FONT = Font(name="Microsoft YaHei", size=10) MONEY_FORMAT = '#,##0.00' THIN_BORDER = Border( left=Side(style='thin', color='D0D0D0'), right=Side(style='thin', color='D0D0D0'), top=Side(style='thin', color='D0D0D0'), bottom=Side(style='thin', color='D0D0D0'), ) COLUMNS = ["日期", "商户/来源", "项目", "金额(元)", "备注"] COL_WIDTHS = [14, 22, 16, 14, 28] def style_header(ws, row): for col_idx, name in enumerate(COLUMNS, 1): cell = ws.cell(row=row, column=col_idx, value=name) cell.font = HEADER_FONT cell.fill = HEADER_FILL cell.alignment = Alignment(horizontal='center', vertical='center') cell.border = THIN_BORDER def style_category_row(ws, row, name, subtotal, count): ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=3) cell = ws.cell(row=row, column=1, value=f"{name}({count}笔)") cell.font = CATEGORY_FONT cell.fill = CATEGORY_FILL cell.border = THIN_BORDER amount_cell = ws.cell(row=row, column=4, value=subtotal) amount_cell.font = CATEGORY_FONT amount_cell.fill = CATEGORY_FILL amount_cell.number_format = MONEY_FORMAT amount_cell.border = THIN_BORDER for c in [2, 3, 5]: ws.cell(row=row, column=c).fill = CATEGORY_FILL ws.cell(row=row, column=c).border = THIN_BORDER def write_item(ws, row, item): values = [ item.get("date", ""), item.get("vendor", ""), item.get("item", ""), item.get("amount", 0), item.get("note", ""), ] for col_idx, val in enumerate(values, 1): cell = ws.cell(row=row, column=col_idx, value=val) cell.font = BODY_FONT cell.border = THIN_BORDER if col_idx == 4: cell.number_format = MONEY_FORMAT cell.alignment = Alignment(horizontal='right') def generate(invoices, output_path): # Group by category grouped = {cat: [] for cat in CATEGORIES} for inv in invoices: cat = inv.get("category", "其他") if cat not in grouped: cat = "其他" grouped[cat].append(inv) wb = Workbook() ws = wb.active ws.title = "发票报销汇总" # Column widths for i, w in enumerate(COL_WIDTHS, 1): ws.column_dimensions[get_column_letter(i)].width = w # Title ws.merge_cells('A1:E1') title_cell = ws.cell(row=1, column=1, value=f"发票报销汇总表") title_cell.font = Font(name="Microsoft YaHei", bold=True, size=14) title_cell.alignment = Alignment(horizontal='center', vertical='center') ws.merge_cells('A2:E2') date_cell = ws.cell(row=2, column=1, value=f"生成日期: {datetime.now().strftime('%Y-%m-%d')}") date_cell.font = Font(name="Microsoft YaHei", size=9, color="888888") date_cell.alignment = Alignment(horizontal='right') row = 4 style_header(ws, row) row += 1 grand_total = 0 for cat_name in CATEGORIES: items = grouped[cat_name] if not items: continue subtotal = sum(i.get("amount", 0) for i in items) grand_total += subtotal style_category_row(ws, row, cat_name, subtotal, len(items)) row += 1 # Sort items by date items.sort(key=lambda x: x.get("date", "")) for item in items: write_item(ws, row, item) row += 1 # Grand total row row += 1 ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=3) total_label = ws.cell(row=row, column=1, value="合计") total_label.font = Font(name="Microsoft YaHei", bold=True, size=12) total_label.alignment = Alignment(horizontal='right') total_label.border = THIN_BORDER total_cell = ws.cell(row=row, column=4, value=grand_total) total_cell.font = Font(name="Microsoft YaHei", bold=True, size=12) total_cell.number_format = MONEY_FORMAT total_cell.border = THIN_BORDER for c in [2, 3, 5]: ws.cell(row=row, column=c).border = THIN_BORDER # Also create a per-category summary sheet ws2 = wb.create_sheet("分类汇总") ws2.column_dimensions['A'].width = 18 ws2.column_dimensions['B'].width = 12 ws2.column_dimensions['C'].width = 14 ws2.cell(row=1, column=1, value="报销类别").font = HEADER_FONT ws2.cell(row=1, column=1).fill = HEADER_FILL ws2.cell(row=1, column=1).border = THIN_BORDER ws2.cell(row=1, column=2, value="笔数").font = HEADER_FONT ws2.cell(row=1, column=2).fill = HEADER_FILL ws2.cell(row=1, column=2).border = THIN_BORDER ws2.cell(row=1, column=3, value="小计(元)").font = HEADER_FONT ws2.cell(row=1, column=3).fill = HEADER_FILL ws2.cell(row=1, column=3).border = THIN_BORDER s_row = 2 for cat_name in CATEGORIES: items = grouped[cat_name] if not items: continue subtotal = sum(i.get("amount", 0) for i in items) ws2.cell(row=s_row, column=1, value=cat_name).font = BODY_FONT ws2.cell(row=s_row, column=1).border = THIN_BORDER ws2.cell(row=s_row, column=2, value=len(items)).font = BODY_FONT ws2.cell(row=s_row, column=2).border = THIN_BORDER c = ws2.cell(row=s_row, column=3, value=subtotal) c.font = BODY_FONT c.number_format = MONEY_FORMAT c.border = THIN_BORDER s_row += 1 ws2.cell(row=s_row, column=1, value="合计").font = CATEGORY_FONT ws2.cell(row=s_row, column=1).border = THIN_BORDER ws2.cell(row=s_row, column=2, value=sum(len(grouped[c]) for c in CATEGORIES)).font = CATEGORY_FONT ws2.cell(row=s_row, column=2).border = THIN_BORDER tc = ws2.cell(row=s_row, column=3, value=grand_total) tc.font = CATEGORY_FONT tc.number_format = MONEY_FORMAT tc.border = THIN_BORDER wb.save(output_path) return output_path, len(invoices), grand_total def main(): parser = argparse.ArgumentParser(description="Generate categorized expense report Excel") parser.add_argument("--input", "-i", required=True, help="JSON file with invoice data") parser.add_argument("--output", "-o", help="Output .xlsx path (default: 发票报销汇总-{date}.xlsx)") args = parser.parse_args() with open(args.input, 'r', encoding='utf-8') as f: data = json.load(f) invoices = data if isinstance(data, list) else data.get("invoices", []) if not args.output: date_str = datetime.now().strftime('%Y%m%d') args.output = f"发票报销汇总-{date_str}.xlsx" out_path, count, total = generate(invoices, args.output) print(f"Generated: {out_path}") print(f" Invoices: {count}") print(f" Total: ¥{total:,.2f}") if __name__ == "__main__": main()
-
-
CHANGELOG.md 651 B
# Changelog All notable changes to this skill are documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning: [SemVer](https://semver.org/) ## [0.2.0] - 2026-08-24 ### Added - add the shared feedback-classification and approval-invalidation gate used by every LovStudio Skill ## [0.1.3] - 2026-05-07 ### Fixed - use AskUserQuestion for expense confirmation ## [0.1.2] - 2026-05-07 ### Fixed - make install path configurable - replace fixed runtime install directory with SKILL_SKILLS_INSTALL_DIR ## [0.1.1] - 2026-05-07 ### Fixed - add release metadata - add README version badge and changelog entry -
README.md 1.5 KB
# 报销管家 · Expense Assistant  Invoice images / text → categorized Excel expense report. ## Install ```bash git clone https://example.com/skills/expense-report-skill \ "${SKILL_SKILLS_INSTALL_DIR:?Set SKILL_SKILLS_INSTALL_DIR}/lov-expense-report" ``` ## Dependencies ```bash pip install openpyxl ``` ## Usage Invoke in Claude Code: ``` /lov-expense-report ``` Then provide invoice images or text descriptions. The skill will: 1. Extract invoice details (date, vendor, amount, type) 2. Auto-classify into expense categories 3. Confirm with you before generating 4. Output a styled Excel report with category subtotals ## Categories | Category | Examples | |----------|----------| | 业务招待 | 客户餐费, 商务宴请, 礼品 | | 差旅-交通 | 机票, 火车票, 打车, 加油 | | 差旅-住宿 | 酒店, 宾馆 | | 差旅-餐饮 | 出差工作餐 | | 办公用品 | 文具, 打印, 办公设备 | | 通讯费 | 话费, 流量, 宽带 | | 其他 | 未分类费用 | ## Script CLI ```bash python scripts/generate_report.py --input invoices.json --output report.xlsx ``` **Input JSON format:** ```json [ { "date": "2026-04-15", "vendor": "海底捞", "item": "客户餐费", "amount": 486.0, "category": "业务招待", "note": "与XX公司李总晚餐" } ] ``` ## Output Excel with two sheets: - **发票报销汇总** — Full itemized report grouped by category - **分类汇总** — Summary: category, count, subtotal ## License MIT -
SKILL.md 6 KB
--- name: lov-expense-report category: Finance tagline: "Invoice images/text → categorized Excel expense report." description: > Extract invoice data from images or text descriptions and generate a categorized Excel expense report. Supports receipt photos, scanned invoices, and manual text input. Auto-classifies into: business entertainment (客户餐费), travel-transport (机票/火车票/打车), travel-accommodation (酒店), travel-meals, office supplies, communication, and other. Use when the user mentions "发票报销", "expense report", "报销单", "发票整理", "invoice", "报销汇总", "发票分类", "reimbursement", or has invoice images to process. license: MIT compatibility: > Requires Python 3.8+ and openpyxl (`pip install openpyxl`). Cross-platform: macOS, Windows, Linux. metadata: author: contributors version: "0.2.0" tags: invoice expense report reimbursement excel categorize --- # 报销管家 · Expense Assistant Extract invoice information from images or text, classify expenses, and generate a professional Excel report with subtotals per category. ## When to Use - User has invoice photos / scanned receipts to process - User describes expenses in text and wants them organized - User needs a categorized reimbursement report (报销单) - User mentions 发票报销, 报销汇总, 发票整理, expense report ## Expense Categories | Category | Keywords / Examples | |----------|-------------------| | 业务招待 | 客户餐费, 商务宴请, 礼品, 招待 | | 差旅-交通 | 机票, 火车票, 高铁, 出租车, 打车, 网约车, 加油, 过路费, 停车费 | | 差旅-住宿 | 酒店, 住宿, 宾馆 | | 差旅-餐饮 | 出差期间餐费, 工作餐 | | 办公用品 | 文具, 打印, 办公耗材, 办公设备 | | 通讯费 | 话费, 流量, 网费, 宽带 | | 其他 | 不属于以上类别的费用 | ## Workflow (MANDATORY) ### Step 1: Collect Invoice Data Accept input in any of these forms: - **Images**: Read invoice photos using the Read tool. Extract: date, vendor, amount, item type, notes. - **Text descriptions**: Parse the user's text for the same fields. - **Mixed**: Multiple images + supplementary text. For each invoice, extract these fields: ```json { "date": "2026-04-15", "vendor": "海底捞(国贸店)", "item": "客户餐费", "amount": 486.00, "category": "业务招待", "note": "与XX公司李总晚餐" } ``` ### Step 2: Classify Assign each invoice to a category from the table above. Rules: 1. If the user explicitly states the category, use it. 2. If the item/vendor clearly matches a category keyword, auto-assign. 3. For ambiguous items (e.g., "餐费" could be 业务招待 or 差旅-餐饮): - If the note mentions a client/customer → 业务招待 - If the context is a business trip → 差旅-餐饮 - If unclear, ask the user. ### Step 3: Confirm with User Before generating, show the extracted data as a table: ``` | # | 日期 | 商户 | 项目 | 金额 | 分类 | 备注 | |---|------|------|------|------|------|------| | 1 | 2026-04-15 | 海底捞 | 客户餐费 | 486.00 | 业务招待 | 与XX公司李总 | | 2 | 2026-04-14 | 滴滴出行 | 打车 | 45.50 | 差旅-交通 | 机场→酒店 | ``` Use `AskUserQuestion` to ask: "以上信息是否正确? 需要修改或补充吗?" ### Step 4: Generate Excel Write the confirmed data to a temp JSON file, then run: ```bash python expense-report-skill/scripts/generate_report.py \ --input /tmp/invoices.json \ --output "发票报销汇总-YYYYMMDD.xlsx" ``` **JSON format** (array of objects): ```json [ {"date": "2026-04-15", "vendor": "海底捞", "item": "客户餐费", "amount": 486.0, "category": "业务招待", "note": "与XX公司李总"}, {"date": "2026-04-14", "vendor": "滴滴出行", "item": "打车", "amount": 45.5, "category": "差旅-交通", "note": "机场→酒店"} ] ``` ### Step 5: Deliver Tell the user: - Output file path - Total amount and breakdown by category - Remind them to review the "分类汇总" sheet for the summary ## Output Format The Excel file contains two sheets: 1. **发票报销汇总** — Full detail, grouped by category with subtotals 2. **分类汇总** — Summary table: category, count, subtotal Style: Skill Publisher warm-academic (terracotta headers #4F46E5, warm cream accents). ## Edge Cases - **Blurry/unreadable image**: Tell the user which fields couldn't be extracted; ask them to provide manually. - **Foreign currency**: Note the currency; convert to CNY if user provides rate, otherwise keep original with note. - **Duplicate invoices**: Flag potential duplicates (same date + vendor + amount) before generating. - **No date on invoice**: Use the date the user provides, or mark as "日期不详". ## Runtime context (shared) 运行前读取本 Skill 包的 `skill.yaml`,由宿主提供 `skill-runtime/v1` 上下文。字段解析顺序为:当前请求、项目上下文、个人 Preferences、品牌 Profile、通用默认值。 - 只使用 Manifest 声明的字段;Profile 保存公开品牌事实,Preferences 保存个人工作偏好。 - `required: true` 字段缺失时,按 Manifest 的问题配置向用户提出一个聚焦问题;用户明确同意后再保存回答。 - 报错提供可复制的 `context_id`、字段路径与来源,诊断内容避开秘密、完整私人路径和原始配置。 ## 通用反馈闭环 用户在 Skill 驱动任务中提出修改意见时,继续当前产物前必须执行: 1. 先判断意见是 `task-specific`(仅本次)还是 `reusable`(可跨任务复用)。 2. `task-specific` 只修改当前任务,不改 Skill。 3. `reusable` 先确定作用域:领域规则先更新对应 canonical Skill;适用于所有 Skill 的规则先更新共享规范。 4. 完成规则更新、版本、lint 与分发核验后,再把修改应用到当前任务。 5. `reusable` 修改会使此前的“确认”“继续”“发吧”失效;完成当前产物修改和回读后必须停下,等待用户下一步指示,不自动进入发布、提交或其他外部写入。 -
skill.yaml 843 B
schema: skill-manifest/v1 id: lov-expense-report version: "0.2.0" runtime: skill-runtime/v1 context: profile: fields: - path: identity.name required: false question: 如果本次输出需要品牌身份,请提供品牌名称。 - path: identity.logo required: false question: 如果需要使用品牌 Logo,请提供 Logo 地址或文件路径。 - path: brand.tone required: false question: 如果已有品牌语气或审美关键词,请提供它们。 preferences: namespace: lov_expense_report fields: - path: user.language required: false question: 希望使用哪种语言输出? - path: user.timezone required: false question: 需要使用哪个时区处理日期和时间? interaction: ask_missing: true max_questions: 1
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.