---
name: invoice-ledger
description: >-
  发票台账与月度报告的产出规范：工作表结构、列定义、人民币金额与日期格式约定、
  汇总口径（作废票与待复核票怎么算）、xlsx 优先与 CSV 降级路径、报告模板。
  Use when 需要生成 / 重建 / 更新发票台账（「出一份台账」「导成 Excel」「这个月一共多少钱」
  「生成月度报告」），或台账数字对不上、要确认某个金额是怎么汇总出来的时候。
  Also covers invoice ledger spreadsheet layout, RMB currency formatting and monthly expense reports.
version: 1.0.0
type: procedural
risk_level: low
status: enabled
tags:
  - invoice
  - ledger
  - spreadsheet
  - report
metadata:
  category: reporting
  i18n:
    default_locale: en-US
    source_locale: zh-CN
    locales:
      - zh-CN
      - en-US
    zh-CN:
      name: 发票台账与报告
      short_desc: 台账工作表结构、人民币金额口径与月度报告模板
    en-US:
      name: Invoice Ledger and Report
      short_desc: Ledger sheet layout, RMB amount conventions and monthly report template
  requires:
    tools:
      - Read
      - Write
---

# 发票台账与报告

## L0

台账每次**全量重建**，数据源永远是 `.index/ledger.json`，不是重新解析文件、也不是在旧台账上追加。

优先出 `.xlsx`；依赖装不上就出**带 UTF-8 BOM 的 CSV**，并明确告诉用户降级了。

## L1

### 重建前先备份

现有台账另存为 `台账.bak.xlsx` / `台账.bak.csv`，再写新的。备份只留最近一份，不做多代。

### 输出格式：先 xlsx，装不上就 CSV

```bash
python3 -c "import openpyxl; import pandas" 2>/dev/null || echo "MISSING"
```

- **不是 MISSING** → 加载市场技能 `xlsx`，按下面的工作表结构生成 `.xlsx`（多工作表、金额格式、冻结首行）
- **MISSING** → 直接 `Write` 一份 CSV，**不要**引导用户去装 Python 依赖打断流程。在收尾里说一句：「Excel 生成依赖（openpyxl / pandas）不可用，已改出 CSV；装上后可以让我重出 xlsx。」

CSV 有两条硬要求：

1. **文件头必须写 UTF-8 BOM（`﻿`）**，否则 Excel 打开中文全是乱码
2. 字段里出现逗号、引号或换行时按 RFC 4180 用双引号包裹并把内部引号翻倍

CSV 只出「明细」一张表，汇总数字放进月度报告的 Markdown 里。

用 `Write` 落盘，不要用 Bash 重定向——`Write` 会产生「本轮修改了哪些文件」卡片，用户能直接点开。

### 工作表结构（xlsx）

四张表，顺序固定。**四张都必须存在，一张都不许省**——哪怕某张只有零行或一行数据，
也要建出表头。台账是要被重复重建、被用户自己写公式引用、被下一次整理覆盖的产物，
表的位置一旦随数据量浮动，「第 3 张是按销售方汇总」这类引用就会在下个月悄悄指错。
（实测踩过：只有 1 张发票时 Agent 判定「按销售方汇总」冗余而略过，产出只有三张表。）


**① 明细** —— 一行一张发票，按开票日期升序

| 列 | 来源字段 | 说明 |
| --- | --- | --- |
| 开票日期 | `invoiceDate` | `YYYY-MM-DD` |
| 发票类型 | `invoiceType` | 数电普票 / 数电专票 / 增值税普通发票 / 铁路电子客票 / 航空行程单 / … |
| 发票号码 | `invoiceNumber` | **文本格式**，见下面的「数字被当成科学计数法」 |
| 发票代码 | `invoiceCode` | 数电票为空 |
| 销售方 | `sellerName` | |
| 销售方税号 | `sellerTaxId` | |
| 购买方 | `buyerName` | |
| 项目 | `items[].name` 拼接 | 多行时用 `；` 连接，超过 60 字截断 |
| 金额 | `amountExcludingTax` | 不含税 |
| 税率 | `taxRate` | 原样，可能是 `6%` / `免税` / `***` |
| 税额 | `taxAmount` | |
| 价税合计 | `totalAmount` | |
| 币种 | `currency` | 默认 `CNY`；外币结算票的票面仍是人民币，这一列照样是 `CNY`，原币信息在备注里 |
| 状态 | 派生 | `正常` / `作废` / `待复核` / `疑似重复` |
| 置信度 | `confidence` | 两位小数 |
| 归档路径 | `archivedPath` | 绝对路径 |
| 来源邮件 | `sourceEmailSubject` | |
| 收件时间 | `sourceReceivedAt` | |

**② 月度汇总** —— 一行一个月份

| 列 | 口径 |
| --- | --- |
| 月份 | `YYYY-MM`，按开票日期归属 |
| 张数 | 计入合计的发票张数 |
| 金额合计 | `amountExcludingTax` 之和 |
| 税额合计 | `taxAmount` 之和 |
| 价税合计 | `totalAmount` 之和 |
| 作废张数 | 单列，不计入上面任何合计 |
| 待复核张数 | 单列，**计入**合计但要能一眼看到 |

**③ 按销售方汇总** —— 一行一个销售方，按价税合计降序。列：销售方 / 税号 / 张数 / 价税合计 / 占比。

**④ 异常** —— 需要人处理的都在这里，一行一条

| 列 | 说明 |
| --- | --- |
| 类型 | `解析失败` / `疑似重复` / `勾稽不符` / `抬头不符` / `低置信度` / `作废` / `范围外日期` |
| 文件 | 归档路径或 `_quarantine/` 路径 |
| 说明 | 一句话讲清卡在哪 |
| 建议动作 | 具体到用户该做什么 |

### 人民币金额与日期约定（**覆盖 `xlsx` 技能的默认约定**）

市场技能 `xlsx` 的财务模型章节写的是美式约定，直接照做会做出一张不像中国发票台账的表。以下五条**以本节为准**：

| 事项 | `xlsx` 技能的默认 | 本台账采用 |
| --- | --- | --- |
| 金额格式 | `$#,##0` | `¥#,##0.00` —— 发票金额精确到分，**不能省略两位小数** |
| 负数 | `($#,##0)` 括号式 | `-¥#,##0.00` 负号式 |
| 零值 | 显示成 `-` | 显示成 `¥0.00` —— 零金额发票是真实存在的票据，隐藏它等于丢账 |
| 表头单位 | `Revenue ($mm)` | 不加单位后缀；币种由「币种」列承载 |
| 日期 | 本地化格式 | 一律 `YYYY-MM-DD` 文本 |

完整数字格式串：`¥#,##0.00;-¥#,##0.00;¥0.00`

### 数字被当成科学计数法

20 位的数电票发票号码、18 位税号写进 Excel 会被识别成数字并显示成 `2.4312E+19`，再也复原不回去。

- xlsx：这两列显式设成文本格式（openpyxl 里 `cell.number_format = '@'` 并写字符串）
- CSV：写成 `="24312000000000020002"` 或在导入说明里让用户把该列指定为文本

税率列同理——`6%` 是文本，别让它变成 `0.06`。

### 汇总口径（唯一定义，别临时发明）

- **作废票不计入任何合计**，单独统计张数
- **待复核票计入合计**，但在「月度汇总」里单列张数，并在异常表里逐条列出
- **疑似重复的两张都计入合计**（在用户确认之前不能替他删掉一张），并在异常表里成对列出
- **外币结算票按票面的人民币金额正常计入合计**。这类票**票面本身就是人民币计价**，`currency` 记 `CNY`（见 `invoice-extract`），原币金额与折算汇率只照抄进备注、不做换算——所以台账里正常情况下不存在非 CNY 的记录，不要写一条「只统计 `currency == 'CNY'`」的过滤当作外币处理办法，那条件永远全真。真的出现票面以非人民币计价的记录时，**不并进任何合计**，在异常表里列成「非人民币计价，需人工处理」，并在报告里单列一段
- **月份按开票日期归属**，不按收件日期。跨年时特别注意：12 月开的票 1 月才收到，仍然算 12 月
- 占比按价税合计算，保留一位小数

### 月度报告

模板见 `${SKILL_DIR}/references/月度报告模板.md`。文件名看**本次入账发票的开票月份
落在几个不同的 `YYYY-MM` 里**（不是用户给的时间范围有多长）：全部同月写
`报告/<YYYY-MM>.md`，分布在两个及以上月份写 `报告/<最早 YYYY-MM>_<最晚 YYYY-MM>.md`。
**不要用运行日期命名**，理由见 `invoice-workflow` 第 7 步。

用户要 PDF/DOCX 时用 `ExportDocument` 从这份 Markdown 转（该工具不能直接出 xlsx）。要把台账文件交给用户时用 `SendUserMessage` 带附件（最多 10 个文件、每个 ≤10MB）。

## L2

### 生成 xlsx 时的注意事项

- 冻结每张表的首行；明细表首行加粗、浅底色
- 「置信度」列低于 0.8 的单元格标橙色底，让待复核项在表里一眼可见
- 不写 Excel 公式。台账的数字来自 `ledger.json`，公式只会引入 `#REF!` 风险且没有任何收益——需要用户自己再算时他可以在 Excel 里加
- 列宽按内容估算，销售方和项目两列给足宽度
- 生成完 `Read` 一次确认文件确实写出来了、行数与 `ledger.json` 的记录数一致

### 台账数字对不上时的排查顺序

1. 先看 `ledger.json` 的记录条数和台账明细行数是否相等——不等说明重建过程丢了记录
2. 再看月度汇总的张数之和是否等于明细行数减去作废张数
3. 再看单张票的 `金额 + 税额` 是否等于 `价税合计`（不等的应该已经进了异常表）
4. 最后才怀疑抽取错误，用 `invoice-extract` 的「回看原图」办法核对票面

**永远不要为了让数字对上去手工改 `ledger.json` 里的值。** 对不上本身就是要报给用户的结论。

### 只重建不重新收集

用户说「重新出一遍台账」时，不要重跑收集和解析——直接读 `ledger.json` 重建。整个过程不碰邮箱，通常几秒完成。

只有用户明确说「重新扫一遍邮箱」时才回到 `invoice-workflow` 的第 2 步。
