# 04 - FR（法国）文档生成说明

> 创建日期：2026-09-23
> 状态：进行中
> 范围：法国 EPR（电池法注册）文档自动生成链路（定时 job）

## 一、概述

法国（FR）链路面向 **EPR 电池法注册**：定时 job 轮询 EPR 源表待推送记录，按国家模板目录下多个模板**逐个渲染、各自上传**（N 模板 → N 文件，**不合并**）。其中 POA 模板（docx）转 PDF，电池分类模板（xlsx）**保留 Excel 原格式**上传；并对法国企业按营业执照号码（SIREN）调法国官方名录查询 NAF/APE 代码回填，电池分类按订单 `CategoryBrandJson` 匹配填写。每个文件单独上传 COS 并回写附件，最后回写推送状态、写生成过程日志。

当前仅有**定时 job**触发（无 API 入口）。

## 二、处理流程

```
调度查询(get_schedule_recode)
        │
        ▼
generate_fr_documents(record, files=[])   # FR 无附件
  1. 确定国家（Country = 'FR'）
  2. 查找模板 find_templates('fr')
  3. _build_values
        ├─ 字段映射（公司/法人/证件/地址…）
        ├─ 法国 → get_naf_code(RegNumber) 查询 NAF/APE（fr_annuaire.py）
        └─ _battery_type_values(CategoryBrandJson) 匹配电池分类
  4. render_templates   逐模板渲染：
        ├─ docx → PDF（POA）
        └─ xlsx → 保留 .xlsx（电池分类表）
  5. 每个文件：upload_file 单独上传 COS + _write_back_annex 回写（按模板 category）
  6. _write_back_status 回写 EPRRegInfo.PushTaxBureauStatus（成功 6 / 失败 -1）
  7. _flush             写生成日志 epr_fr_battery_file_log（单行合并）
```

关键点：

- FR **无附件**（`files=[]`），`_prepare_images` 拿不到营业执照/身份证，OCR 基本走空。
- N 模板 → N 文件，每个文件独立命名、独立上传、独立回写（分类按 `FR_TEMPLATE_CONFIG`）。
- 电池分类匹配见「电池分类匹配」小节。

## 三、数据表

### 源库（只读）

| 表 | 用途 |
| --- | --- |
| `EPRBusinessRecord` | EPR 业务登记记录（`Country`、`BusinessSerialNumber`、`VATNumber`、`MSOrderDetailsId`） |
| `EPRRegInfo` | EPR 注册信息（回写列 `PushTaxBureauStatus`） |
| `Base_Customer_Company` | 企业信息 |
| `ServiceItems` | 服务项（`ServiceItemName`，过滤「电池法注册」） |
| `Country` | 国家（`CountryName_en`） |
| `MS_OrderDetails` | 订单明细（`CategoryBrandJson` 电池分类品牌 JSON） |

调度 SQL 见 `app/repositories/fr_enterprise.py::_FR_SCHEDULE_SQL`，当前测试条件硬编码：

```sql
br.Country='FR'
  AND br.BusinessSerialNumber='POEPR20260916000012'
  AND ri.PushType='301'
  AND si.ServiceItemName='电池法注册'
```

### 目标库（rpa，本系统所有）

- `epr_fr_battery_file_log`：生成过程日志（见「日志表」）。

## 四、模板与占位符

- 模板目录：`code/templates/fr/`。
  - `2026年电池POA模板.docx` → 渲染后转 PDF，输出 `..._POA.pdf`。
  - `TEMPLATE data new registrations battery categories.xlsx` → 渲染后**保留 `.xlsx`** 上传，输出 `..._battery_categories.xlsx`。
- 占位符形式同 NL（`app/services/placeholder.py`）：`{{key}}`、`《key》` 等。
- 字段值由 `_build_values` 映射（含 `NAF Code`、5 个电池分类占位符等）。

## 五、电池分类匹配（_battery_type_values）

- 输入：`MS_OrderDetails.CategoryBrandJson`（list，每项含 `eprCategory`、`eprCategoryName`、`brandName` 等）。
- 映射：`_BATTERY_TYPE_MAP`（恒等映射，`eprCategoryName` 即电池分类名）→ 5 个占位符（便携式电池 / 轻型交通工具用电池 / 工业电池 / 启动、照明和点火电池 / 电动汽车电池），命中填 `YES`。
- 两方案（`app/services/enterprise_document.py`）：
  - **方案一（保留备用）**：直接取 JSON 里的 `eprCategoryName` 匹配。
  - **方案二（现启用）**：按 `eprCategory` 查 `EPRCategory` 表取名称再匹配（`_USE_EPR_CATEGORY_DB=True` 切换，查询函数在 `fr_enterprise.get_epr_category_names`）。

## 六、NAF/APE 查询（fr_annuaire.py）

- 触发：`_build_values` 内 `_is_france(CompanyCountry)` 为真时，用 `RegNumber`（SIREN/SIRET）查询。
- 数据源：法国官方名录 API `https://recherche-entreprises.api.gouv.fr/search?q=<siren>`（免鉴权，即 `annuaire-entreprises.data.gouv.fr` 的底层数据源）。
- 取值：`activite_principale`（单位法人）→ `siege.activite_principale`（总机构）→ `matching_etablissements[0].activite_principale` 兜底。
- 失败/未查到返回空、只记 warning，**不阻断生成**；依赖部署环境能访问该法国站点。

## 七、输出与 COS

- N 个文件，每个命名：`{gen_file_prefix}{企业名}_{cfg['name']}{gen_file_suffix}{ext}`（`cfg` 来自 `FR_TEMPLATE_CONFIG`，`ext` 保留源格式 `.pdf` / `.xlsx`）。
- 上传：`cos_storage.upload_file`，按扩展名设置 Content-Type（`.xlsx` → spreadsheetml）、带 Content-Disposition。
- 回写：每个文件按模板的 `category` 回写 `Base_AnnexesFile`。

## 八、日志表（epr_fr_battery_file_log）

- 写入函数：`app/repositories/epr_generate_log.py::insert_epr_generate_log`。
- 语义同 NL 的 `vat_nl_file_log`：**每 code 一行**，`step=NULL`，`message` 合并所有步骤、`detail` 合并异常、`level` 取最高级别，每次整体覆盖更新，`updated_at` 一致。

## 九、代码结构

| 文件 | 职责 |
| --- | --- |
| `app/jobs/generate_documents_job_fr.py` | 轮询 job 入口（`run` / `run_once`） |
| `app/services/fr_document.py` | 编排 `generate_fr_documents` |
| `app/repositories/fr_enterprise.py` | FR 源库 SQL 查询 / 状态回写 / `FR_TEMPLATE_CONFIG` / `get_epr_category_names` |
| `app/repositories/epr_generate_log.py` | 写日志 `epr_fr_battery_file_log` |
| `app/services/fr_annuaire.py` | 法国名录查询 NAF/APE |
| `app/services/enterprise_document.py`（共享） | 字段映射 / 图片 / OCR / 模板渲染 / 电池分类 / 回写 helper |
| `app/services/cos_storage.py`（共享） | COS 上传 |
| `app/services/converter.py`（共享） | docx/xlsx → PDF（COM / LibreOffice） |
| `app/core/countries.py`（共享） | 国家代码映射 + 模板查找 |

## 十、配置项（.env）

与 NL 相同（数据库 / OCR / COS / PDF_CONVERTER / job 间隔），额外无 FR 专属配置。注意：NAF/APE 查询需要部署环境能访问 `recherche-entreprises.api.gouv.fr`（境内服务器可能需要代理，不通则静默返回空、不阻断）。

## 十一、部署与使用

### 通用前置

- Python 3.10+（建议 3.11/3.12）。
- `pip install -r requirements.txt`。
- 配置 `.env`（复制 `.env.example`）。
- 目标库 `epr_fr_battery_file_log` 需手动建表（结构同 `vat_nl_file_log`，`step` 允许 NULL）。

### Windows

1. 安装 Python 3.10+、Microsoft Office（Word + Excel）、SQL Server ODBC Driver 17/18。
2. `pip install -r requirements.txt`。
3. `PDF_CONVERTER=auto` 或 `msoffice`。
4. 跑 FR job（`code` 目录，PowerShell）：
   ```powershell
   python -m app.jobs.generate_documents_job_fr --once
   ```

#### 注册为 Windows 任务计划程序（开机守护，生产推荐）

FR 使用脚本 `code/win_bat/epr_fr_file_start_job.bat`（与 NL 仅差一行，跑 FR job；若尚未创建，复制 NL 脚本改名、把命令换成 `generate_documents_job_fr` 即可）：

```bat
@echo off
cd /d "%~dp0.."
python -m app.jobs.generate_documents_job_fr
```

**职责划分**：任务计划程序只做「进程守护」（开机启动一次、崩溃自动重启），不负责定时频率；业务调度由 job 内部 `while True` 轮询负责（`JOB_POLL_SECONDS` / `JOB_EMPTY_SLEEP_SECONDS`）。

> ⚠️ 常驻模式（不带 `--once`）下，触发器**不要**设「每 N 分钟重复」，否则多实例叠加重复处理。要由任务计划程序掌握频率，改用 `--once` 一次性模式。

步骤（与 NL 完全一致，仅脚本路径/命令不同）：

1. 准备启动脚本 `code/win_bat/epr_fr_file_start_job.bat`（内容见上）。
2. `Win+R` → `taskschd.msc` →「创建任务…」（非「创建基本任务」）。
3. 配置：
   - 常规：名称如 `epr-fr-doc-job`；勾选「不管用户是否登录都要运行」；配置选 Windows 10/11。
   - 触发器：新建「启动时」；可加「任务失败后：每 1 分钟重启，最多 3 次」作崩溃恢复。
   - 操作：新建「启动程序」→ 程序/脚本填 `E:\...\code\win_bat\epr_fr_file_start_job.bat`，「起始于」填 `E:\...\code\win_bat`。
   - 条件：取消勾选「只有在计算机使用交流电源时才启动此任务」。
4. 环境变量：脚本默认读 `code\.env`；生产配置取消 `rem set APP_ENV=prod` 的注释。
5. 若 `python` 不在运行账户 PATH，改为 python.exe 绝对路径。
6. 验证：右键任务「运行」，观察 `logs\` 日志；或先 `python -m app.jobs.generate_documents_job_fr --once` 冒烟。

### Linux（Debian/Ubuntu）

1. 安装 LibreOffice 与中文字体：
   ```bash
   sudo apt-get install -y libreoffice fonts-noto-cjk
   ```
2. 若 SQL Server：`sudo apt-get install -y unixodbc unixodbc-dev` + `msodbcsql18` + `pip install pyodbc`。
3. `pip install -r requirements.txt`。
4. `PDF_CONVERTER=libreoffice`，`SOFFICE_PATH=soffice`。
5. 跑 FR job：
   ```bash
   cd code && python -m app.jobs.generate_documents_job_fr --once
   ```

## 十二、注意事项

- **xlsx 保留原格式上传**：电池分类模板不会被转成 PDF；`render_templates` 中 xlsx 分支刻意保留 `.xlsx`（不要改回 `pdf_paths.append(out_pdf)`）。
- NAF/APE 查询依赖访问法国名录 API；境内服务器可能需要代理。
- 调度 SQL 含测试硬编码（`BusinessSerialNumber='...'`），上线前需移除。
- 电池分类方案二为保留代码，启用前需核对 `EPRCategory` 实际表名/主键/名称列。
