Files
invoice-master-poc-v2/docs/FORTNOX_INTEGRATION_SPEC.md
2026-02-01 22:40:41 +01:00

1691 lines
62 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Invoice Master - Fortnox Integration Technical Specification
**版本**: v1.0
**日期**: 2026-02-01
**作者**: Claude Code
**状态**: 设计阶段
---
## 目录
1. [概述](#概述)
2. [集成模式说明](#集成模式说明)
3. [系统架构](#系统架构)
4. [Fortnox API分析](#fortnox-api分析)
5. [数据映射设计](#数据映射设计)
6. [核心功能模块](#核心功能模块)
7. [用户流程设计](#用户流程设计)
8. [UI设计规范](#ui设计规范)
9. [API设计](#api设计)
10. [数据库设计](#数据库设计)
11. [安全设计](#安全设计)
12. [错误处理](#错误处理)
13. [开发计划](#开发计划)
14. [测试策略](#测试策略)
15. [部署方案](#部署方案)
16. [附录](#附录)
---
## 概述
### 1.1 项目背景
Invoice Master是一个基于YOLOv11 + PaddleOCR的发票字段自动提取系统当前准确率达到94.8%。本方案设计将Invoice Master作为Fortnox会计软件的插件/扩展,实现无缝的发票数据导入功能。
### 1.2 目标
- 为Fortnox用户提供智能发票识别功能
- 实现一键将发票数据导入Fortnox
- 自动匹配供应商和会计科目
- 减少90%的手动录入工作
### 1.3 范围
**包含功能:**
- Fortnox OAuth2认证集成
- 发票PDF上传和OCR识别
- 供应商自动匹配/创建
- 会计凭证(Voucher)自动生成
- 发票图像存档
**不包含功能 (Phase 2):**
- 多文档类型支持 (收据、对账单)
- 自动付款流程
- 审批工作流
### 1.4 术语定义
| 术语 | 英文 | 说明 |
|------|------|------|
| 供应商 | Supplier | Leverantör i Fortnox |
| 会计凭证 | Voucher | Verifikation i Fortnox |
| 发票 | Invoice | Faktura |
| 科目 | Account | Konto i kontoplanen |
| OCR参考号 | OCR Number | 瑞典特有的付款参考号 |
---
## 集成模式说明
### 2.1 Fortnox Extension UI模式
Fortnox的集成主要有**两种模式**Invoice Master采用**模式1: 外部独立应用**。
#### 模式1: 外部独立应用 (External App) - 推荐
**架构示意图:**
```
用户流程:
┌─────────────────┐ ┌─────────────────────┐ ┌─────────────────┐
│ Fortnox │────▶│ Invoice Master │────▶│ Fortnox │
│ (点击集成) │ │ (独立Web应用) │ │ (数据已导入) │
└─────────────────┘ └─────────────────────┘ └─────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
在Fortnox中 用户在你的网站上 用户回到Fortnox
看到"Invoice Master" 完成发票上传和识别 查看已导入的凭证
点击打开新窗口
```
**特点:**
-**有自己的完整UI**(独立网站)
- ✅ 通过OAuth2连接Fortnox
- ✅ 用户在Fortnox点击后跳转到你的网站
- ✅ 数据通过API双向同步
- ✅ 更灵活的功能和用户体验
**Fortnox中的展示:**
- 在Fortnox Integrations页面列出
- 用户点击后打开新标签页到你的网站
- 显示连接状态和基本设置
#### 模式2: 嵌入式集成 (Embedded) - 有限支持
**Fortnox目前支持:**
- 菜单链接 (Menu Links) - 在Fortnox菜单中添加链接
- 快捷操作 (Quick Actions) - 有限的上下文操作
- 文件导入 (File Import) - 通过Inbox API
**Fortnox不提供:**
- ❌ iframe嵌入第三方UI
- ❌ 自定义页面/标签
- ❌ 深度UI定制
### 2.2 推荐方案: 混合模式
**架构设计:**
```
┌─────────────────────────────────────────────────────────────────┐
│ Invoice Master for Fortnox │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 独立Web应用 (你的域名) │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ 发票上传 │ │ 识别结果 │ │ 历史记录 │ │ │
│ │ │ 页面 │ │ 确认页面 │ │ 页面 │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ │ │
│ │ 功能: OCR识别、供应商匹配、预览确认、一键导入Fortnox │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ │ HTTPS API │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Fortnox Integration Service │ │
│ │ (Backend API) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
└──────────────────────────────┼───────────────────────────────────┘
│ OAuth2 + REST API
┌──────────▼──────────┐
│ Fortnox │
│ (数据存储/展示) │
└─────────────────────┘
```
### 2.3 与纯API方案对比
| 特性 | 独立UI方案 (推荐) | 纯API方案 |
|------|------------------|-----------|
| **用户体验** | ⭐⭐⭐⭐⭐ 完整的可视化界面 | ⭐⭐ 需要用户自己调用API |
| **开发复杂度** | ⭐⭐⭐ 需要前端+后端 | ⭐⭐ 只需要后端API |
| **功能灵活性** | ⭐⭐⭐⭐⭐ 可以做OCR预览、编辑 | ⭐⭐ 直接导入,无法预览 |
| **用户门槛** | ⭐⭐⭐⭐⭐ 低,非技术用户可用 | ⭐ 高,需要开发者 |
| **Fortnox审核** | ⭐⭐⭐⭐ 标准流程 | ⭐⭐⭐⭐ 更简单 |
### 2.4 用户完整流程
```
1. 发现阶段
用户在Fortnox Integrations页面找到"Invoice Master"
2. 授权阶段
用户点击"连接" → OAuth2授权 → 跳转到Invoice Master
3. 使用阶段 (在Invoice Master网站)
上传PDF → OCR识别 → 确认/编辑 → 导入到Fortnox
4. 查看阶段 (回到Fortnox)
用户在Fortnox中查看已导入的凭证和发票
```
---
## 系统架构
### 3.1 整体架构图
**注意: 这是技术架构图对应第2章描述的"独立Web应用"模式**
```
┌─────────────────────────────────────────────────────────────────┐
│ Fortnox Platform │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Fortnox │ │ Fortnox │ │ Fortnox │ │
│ │ UI │ │ API │ │ Database │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │
└─────────┼─────────────────┼─────────────────────────────────────┘
│ │
│ OAuth2 │ HTTPS
│ │
┌─────────▼─────────────────▼─────────────────────────────────────┐
│ Invoice Master Integration │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Fortnox Integration Service │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ Auth │ │ Invoice │ │ Supplier │ │ │
│ │ │ Module │ │ Handler │ │ Matcher │ │ │
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ Voucher │ │ File │ │ Webhook │ │ │
│ │ │ Creator │ │ Storage │ │ Handler │ │ │
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Invoice Master Core Services │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ OCR │ │ YOLO │ │ Field │ │ │
│ │ │ Engine │ │ Detector │ │Normalizer │ │ │
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ PostgreSQL / Azure Blob
┌─────────▼─────────────────────────────────────────────────────┐
│ Data Storage │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Invoice │ │ Fortnox │ │ File │ │
│ │ Data │ │ Tokens │ │ Storage │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### 3.2 组件说明
| 组件 | 技术栈 | 职责 |
|------|--------|------|
| **Integration Service** | FastAPI + Python | Fortnox API交互、业务逻辑 |
| **Auth Module** | OAuth2 + JWT | Fortnox认证、Token管理 |
| **Invoice Handler** | - | 发票处理流程协调 |
| **Supplier Matcher** | Fuzzy Matching | 供应商匹配算法 |
| **Voucher Creator** | - | 生成Fortnox会计凭证 |
| **File Storage** | Azure Blob / S3 | 发票PDF存储 |
| **Webhook Handler** | - | 接收Fortnox事件 |
### 3.3 技术栈
| 层级 | 技术 | 说明 |
|------|------|------|
| **Backend** | FastAPI + Python 3.11 | API服务 |
| **Database** | PostgreSQL 15 | 关系数据 |
| **Cache** | Redis | Token缓存、限流 |
| **Storage** | Azure Blob Storage | 文件存储 |
| **Message Queue** | Redis Queue | 异步任务 |
| **Monitoring** | Prometheus + Grafana | 监控告警 |
---
## Fortnox API分析
### 3.1 认证机制
Fortnox使用OAuth2授权码流程:
```
┌─────────┐ ┌─────────────┐
│ User │──(1) Authorization Request───────▶│ Fortnox │
│ │◀──(2) Authorization Code─────────│ OAuth2 │
│ │ │ Server │
│ │──(3) Token Request────────────────▶│ │
│ │◀──(4) Access + Refresh Token──────│ │
└─────────┘ └─────────────┘
```
**关键端点:**
```
Authorization URL: https://apps.fortnox.se/oauth-v1/auth
Token URL: https://apps.fortnox.se/oauth-v1/token
API Base URL: https://api.fortnox.se/3
```
**Scopes Required:**
```
supplier - 供应商管理
invoice - 发票管理 (如需要)
voucher - 会计凭证
account - 会计科目
companyinformation - 公司信息
```
### 3.2 核心API端点
#### 3.2.1 供应商管理
```http
# 获取供应商列表
GET /3/suppliers
Response: {
"Suppliers": [
{
"@url": "https://api.fortnox.se/3/suppliers/123",
"Name": "ABC Company",
"SupplierNumber": "123",
"OrganisationNumber": "556677-8899"
}
]
}
# 创建供应商
POST /3/suppliers
Body: {
"Supplier": {
"Name": "New Supplier",
"OrganisationNumber": "112233-4455"
}
}
```
#### 3.2.2 会计凭证 (Voucher)
```http
# 创建会计凭证
POST /3/vouchers
Body: {
"Voucher": {
"VoucherSeries": "A", // 凭证系列
"TransactionDate": "2024-01-15", // 交易日期
"VoucherRows": [
{
"Account": 2440, // 应付账款科目
"Debit": 1250.00,
"Credit": 0,
"Description": "Invoice F2024-001"
},
{
"Account": 5460, // 费用科目
"Debit": 0,
"Credit": 1000.00,
"Description": "Office supplies"
},
{
"Account": 2610, // 增值税科目
"Debit": 0,
"Credit": 250.00,
"Description": "VAT 25%"
}
]
}
}
```
#### 3.2.3 文件上传
```http
# 上传附件到Fortnox
POST /3/inbox
Content-Type: multipart/form-data
Body: {
"file": [PDF file],
"name": "Invoice_F2024_001.pdf"
}
```
### 3.3 API限制
| 限制类型 | 值 | 说明 |
|---------|-----|------|
| 速率限制 | 300请求/分钟 | 超出返回429 |
| 并发连接 | 10 | 同时连接数 |
| Token有效期 | 3600秒 | 需使用Refresh Token |
| 文件大小 | 10MB | 单个文件限制 |
---
## 数据映射设计
### 4.1 发票字段映射
**Invoice Master提取字段 → Fortnox字段**
| Invoice Master | Fortnox | 类型 | 必填 | 转换逻辑 |
|---------------|---------|------|------|----------|
| `invoice_number` | `ExternalInvoiceNumber` | string | 是 | 直接映射 |
| `invoice_date` | `TransactionDate` | date | 是 | ISO 8601格式 |
| `due_date` | `DueDate` | date | 否 | 计算或提取 |
| `supplier_name` | `SupplierName` | string | 是 | 匹配或创建 |
| `supplier_org_number` | `SupplierOrganisationNumber` | string | 否 | 用于匹配 |
| `amount_total` | `TotalAmount` | decimal | 是 | 直接映射 |
| `amount_vat` | `VatAmount` | decimal | 否 | 计算得出 |
| `ocr_number` | `OCRNumber` | string | 否 | 瑞典特有 |
| `bankgiro` | `BankgiroNumber` | string | 否 | 付款信息 |
| `plusgiro` | `PlusgiroNumber` | string | 否 | 付款信息 |
| `currency` | `Currency` | string | 是 | 默认SEK |
### 4.2 会计科目映射
**默认科目映射表 (Kontoplan BAS2024)**
| 费用类型 | 科目代码 | 科目名称 | 说明 |
|---------|---------|---------|------|
| 应付账款 | 2440 | Leverantörsskulder | 默认贷方 |
| 办公用品 | 5460 | Kontorsmaterial | 常见费用 |
| 咨询服务 | 6210 | Konsultarvoden | 外部服务 |
| 运输费 | 5710 | Frakter | 物流费用 |
| 增值税进项 | 2610 | Ingående moms | 25% VAT |
| 增值税进项12% | 2620 | Ingående moms 12% | 食品等 |
| 增值税进项6% | 2630 | Ingående moms 6% | 交通等 |
**科目选择逻辑:**
```python
def select_account(invoice_data: dict) -> int:
"""根据发票内容选择会计科目"""
# 1. 检查是否有历史映射
if invoice_data['supplier_org_number']:
historical = get_historical_account(invoice_data['supplier_org_number'])
if historical:
return historical
# 2. 关键词匹配
description = invoice_data.get('description', '').lower()
if any(word in description for word in ['kontor', 'papper', 'penna']):
return 5460 # 办公用品
elif any(word in description for word in ['konsult', 'tjänst']):
return 6210 # 咨询服务
elif any(word in description for word in ['frakt', 'transport']):
return 5710 # 运输费
# 3. 默认科目
return 6100 # 其他外部费用
```
### 4.3 供应商匹配算法
**匹配优先级:**
```python
class SupplierMatcher:
def match_supplier(self, extracted_data: dict) -> MatchResult:
"""
供应商匹配算法
返回: (supplier_number, confidence_score, action)
"""
# 1. 组织号精确匹配 (权重: 100%)
if extracted_data.get('supplier_org_number'):
exact_match = self.find_by_org_number(
extracted_data['supplier_org_number']
)
if exact_match:
return MatchResult(
supplier_number=exact_match.number,
confidence=1.0,
action='USE_EXISTING'
)
# 2. 名称模糊匹配 (权重: 80%)
name_matches = self.fuzzy_match_name(
extracted_data['supplier_name'],
threshold=0.85
)
if name_matches and name_matches[0].score > 0.9:
return MatchResult(
supplier_number=name_matches[0].number,
confidence=name_matches[0].score,
action='USE_EXISTING'
)
# 3. 建议创建新供应商 (权重: <80%)
return MatchResult(
supplier_number=None,
confidence=0.0,
action='CREATE_NEW',
suggested_name=extracted_data['supplier_name']
)
```
---
## 核心功能模块
### 5.1 认证模块 (Auth Module)
**职责:**
- Fortnox OAuth2流程管理
- Token存储和刷新
- 多租户隔离
**核心类:**
```python
class FortnoxAuthManager:
"""Fortnox认证管理器"""
def __init__(self, client_id: str, client_secret: str):
self.client_id = client_id
self.client_secret = client_secret
self.token_store = TokenStore()
def get_authorization_url(self, state: str) -> str:
"""生成Fortnox授权URL"""
params = {
'client_id': self.client_id,
'redirect_uri': settings.FORTNOX_REDIRECT_URI,
'scope': 'supplier voucher account companyinformation',
'state': state,
'response_type': 'code'
}
return f"{FORTNOX_AUTH_URL}?{urlencode(params)}"
async def exchange_code_for_token(self, code: str) -> FortnoxToken:
"""用授权码换取Token"""
response = await httpx.post(
FORTNOX_TOKEN_URL,
auth=(self.client_id, self.client_secret),
data={
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': settings.FORTNOX_REDIRECT_URI
}
)
token_data = response.json()
return FortnoxToken(
access_token=token_data['access_token'],
refresh_token=token_data['refresh_token'],
expires_at=datetime.utcnow() + timedelta(seconds=token_data['expires_in']),
scope=token_data['scope']
)
async def get_valid_access_token(self, tenant_id: str) -> str:
"""获取有效的访问Token自动刷新"""
token = await self.token_store.get(tenant_id)
if token.is_expired():
token = await self.refresh_token(token.refresh_token)
await self.token_store.save(tenant_id, token)
return token.access_token
```
### 5.2 发票处理模块 (Invoice Handler)
**处理流程:**
```python
class InvoiceProcessingService:
"""发票处理服务"""
async def process_invoice(
self,
tenant_id: str,
pdf_file: UploadFile,
settings: ProcessingSettings
) -> ProcessingResult:
"""
处理发票的主流程
"""
# 1. 保存PDF文件
file_path = await self.file_storage.save(pdf_file)
# 2. OCR提取
extraction_result = await self.ocr_service.extract(file_path)
# 3. 验证提取结果
if not self.validate_extraction(extraction_result):
return ProcessingResult(
status='FAILED',
error='Extraction validation failed'
)
# 4. 供应商匹配
supplier_match = await self.supplier_matcher.match(
tenant_id,
extraction_result
)
# 5. 创建或获取供应商
if supplier_match.action == 'CREATE_NEW':
supplier_number = await self.create_supplier(
tenant_id,
extraction_result
)
else:
supplier_number = supplier_match.supplier_number
# 6. 生成会计凭证
voucher = await self.voucher_creator.create(
tenant_id,
extraction_result,
supplier_number,
settings
)
# 7. 上传附件
if settings.attach_pdf:
await self.attach_invoice_pdf(tenant_id, voucher.id, file_path)
return ProcessingResult(
status='SUCCESS',
extraction=extraction_result,
supplier_number=supplier_number,
voucher_id=voucher.id,
confidence=supplier_match.confidence
)
```
### 5.3 供应商匹配模块 (Supplier Matcher)
```python
class FortnoxSupplierMatcher:
"""Fortnox供应商匹配器"""
def __init__(self, fortnox_client: FortnoxClient):
self.client = fortnox_client
self.cache = SupplierCache()
async def match(
self,
tenant_id: str,
extraction: ExtractionResult
) -> SupplierMatchResult:
"""匹配供应商"""
# 获取所有供应商(带缓存)
suppliers = await self.cache.get_suppliers(tenant_id)
# 1. 组织号精确匹配
if extraction.supplier_org_number:
match = self._match_by_org_number(
suppliers,
extraction.supplier_org_number
)
if match:
return SupplierMatchResult(
supplier_number=match['SupplierNumber'],
confidence=1.0,
action='USE_EXISTING'
)
# 2. 名称模糊匹配
name_match = self._fuzzy_match_name(
suppliers,
extraction.supplier_name
)
if name_match and name_match['score'] > 0.9:
return SupplierMatchResult(
supplier_number=name_match['supplier']['SupplierNumber'],
confidence=name_match['score'],
action='USE_EXISTING'
)
elif name_match and name_match['score'] > 0.7:
return SupplierMatchResult(
supplier_number=name_match['supplier']['SupplierNumber'],
confidence=name_match['score'],
action='SUGGEST_MATCH',
suggested_name=extraction.supplier_name
)
# 3. 建议创建新供应商
return SupplierMatchResult(
supplier_number=None,
confidence=0.0,
action='CREATE_NEW',
suggested_name=extraction.supplier_name,
suggested_org_number=extraction.supplier_org_number
)
async def create_supplier(
self,
tenant_id: str,
extraction: ExtractionResult
) -> str:
"""在Fortnox中创建新供应商"""
supplier_data = {
'Supplier': {
'Name': extraction.supplier_name,
'OrganisationNumber': extraction.supplier_org_number,
'Address1': extraction.supplier_address,
'Phone': extraction.supplier_phone,
'Email': extraction.supplier_email,
'BankgiroNumber': extraction.bankgiro,
'PlusgiroNumber': extraction.plusgiro
}
}
response = await self.client.post(
tenant_id,
'/3/suppliers',
json=supplier_data
)
# 刷新缓存
await self.cache.invalidate(tenant_id)
return response['Supplier']['SupplierNumber']
```
### 5.4 凭证生成模块 (Voucher Creator)
```python
class FortnoxVoucherCreator:
"""Fortnox会计凭证生成器"""
async def create_voucher(
self,
tenant_id: str,
extraction: ExtractionResult,
supplier_number: str,
settings: VoucherSettings
) -> VoucherResult:
"""创建会计凭证"""
# 确定会计科目
account = await self.select_account(extraction)
# 计算VAT
vat_amount = self.calculate_vat(
extraction.amount_total,
extraction.vat_rate or 25
)
amount_excl_vat = extraction.amount_total - vat_amount
# 构建凭证行
voucher_rows = [
# 借方: 费用科目
{
'Account': account,
'Debit': amount_excl_vat,
'Credit': 0,
'Description': f"{extraction.supplier_name} - {extraction.invoice_number}",
'Project': settings.project_code
},
# 借方: 增值税
{
'Account': self.get_vat_account(extraction.vat_rate),
'Debit': vat_amount,
'Credit': 0,
'Description': f"Moms {extraction.vat_rate}%"
},
# 贷方: 应付账款
{
'Account': 2440, # Leverantörsskulder
'Debit': 0,
'Credit': extraction.amount_total,
'Description': f"Faktura {extraction.invoice_number}",
'SupplierNumber': supplier_number,
'OCRNumber': extraction.ocr_number
}
]
voucher_data = {
'Voucher': {
'VoucherSeries': settings.voucher_series or 'A',
'TransactionDate': extraction.invoice_date.isoformat(),
'VoucherText': f"Inköp {extraction.supplier_name}",
'VoucherRows': voucher_rows
}
}
response = await self.client.post(
tenant_id,
'/3/vouchers',
json=voucher_data
)
return VoucherResult(
voucher_id=response['Voucher']['VoucherNumber'],
series=response['Voucher']['VoucherSeries'],
url=response['Voucher']['@url']
)
```
---
## 用户流程设计
### 6.1 集成入口点 (在Fortnox中)
**Fortnox Integrations页面展示:**
```
┌─────────────────────────────────────────────────────────────┐
│ Invoice Master - Smart Invoice OCR │
│ │
│ 📄 自动识别发票信息 │
│ 🤖 AI驱动的OCR技术 │
│ ⚡ 一键导入到Fortnox │
│ │
│ [连接/打开] │
└─────────────────────────────────────────────────────────────┘
```
**Fortnox内配置页面:**
```
┌─────────────────────────────────────────────────────────────┐
│ Invoice Master 设置 │
│ │
│ 状态: ✅ 已连接 │
│ 公司: My Company AB │
│ │
│ 默认设置: │
│ - 凭证系列: [A ▼] │
│ - 自动导入: [✓] │
│ - 附件上传: [✓] │
│ │
│ [保存设置] [断开连接] │
└─────────────────────────────────────────────────────────────┘
```
### 6.2 首次设置流程
```
用户点击"连接Fortnox"
┌───────────────────┐
│ 跳转到Fortnox授权页 │
│ (OAuth2流程) │
└─────────┬─────────┘
┌───────────────────┐
│ 用户登录Fortnox │
│ 并授权访问 │
└─────────┬─────────┘
┌───────────────────┐
│ 返回到Invoice │
│ Master回调页面 │
└─────────┬─────────┘
┌───────────────────┐
│ 获取公司信息 │
│ 验证连接成功 │
└─────────┬─────────┘
┌───────────────────┐
│ 配置默认设置 │
│ - 凭证系列 │
│ - 默认科目 │
│ - 文件存储选项 │
└─────────┬─────────┘
┌───────────────────┐
│ 完成!显示 │
│ 上传发票界面 │
└───────────────────┘
```
### 6.2 发票处理流程
```
用户上传PDF发票
┌───────────────────┐
│ 显示处理进度 │
│ - OCR提取中... │
└─────────┬─────────┘
┌───────────────────┐
│ 显示提取结果 │
│ 供用户确认/编辑 │
│ │
│ ┌───────────────┐ │
│ │ 供应商: XXX │ │
│ │ 金额: 1,250 │ │
│ │ 日期: 2024... │ │
│ │ [编辑] [确认] │ │
│ └───────────────┘ │
└─────────┬─────────┘
┌─────┴─────┐
│ │
▼ ▼
┌────────┐ ┌────────┐
│ 编辑 │ │ 确认 │
│ 数据 │ │ 导入 │
└───┬────┘ └───┬────┘
│ │
└─────┬─────┘
┌───────────────────┐
│ 供应商匹配 │
│ - 查找现有 │
│ - 或创建新 │
└─────────┬─────────┘
┌───────────────────┐
│ 生成会计凭证 │
│ 上传到Fortnox │
└─────────┬─────────┘
┌───────────────────┐
│ 显示成功消息 │
│ 提供Fortnox链接 │
│ 查看凭证 │
└───────────────────┘
```
### 6.4 独立Web应用UI设计
**重要说明: 以下UI是Invoice Master独立Web应用的界面用户在Fortnox点击"打开"后跳转到此界面。**
#### 主界面
#### 主界面
```
┌─────────────────────────────────────────────────────────────┐
│ Invoice Master for Fortnox [⚙️设置] [?] │
├─────────────────────────────────────────────────────────────┤
│ │
│ 📤 上传发票 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 拖放PDF文件到这里 │ │
│ │ 或点击选择文件 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ 📋 最近处理的发票 │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 文件名 │ 供应商 │ 金额 │ 状态 │ │
│ ├───────────────────────────────────────────────────────┤ │
│ │ INV001.pdf │ ABC Company │ 1,250 │ ✅ 已导入 │ │
│ │ INV002.pdf │ XYZ AB │ 3,450 │ ✅ 已导入 │ │
│ │ INV003.pdf │ (未匹配) │ 890 │ ⚠️ 待确认 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
#### 结果确认界面
**说明: 此界面在Invoice Master独立Web应用中显示用于用户确认OCR识别结果。
```
┌─────────────────────────────────────────────────────────────┐
│ 确认发票信息 [✕] [✓] │
├─────────────────────────────────────────────────────────────┤
│ │
│ 📄 Invoice_F2024_001.pdf │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 供应商信息 │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ 名称: ABC Company │ │ │
│ │ │ 组织号: 556677-8899 │ │ │
│ │ │ 状态: ✅ 已匹配现有供应商 │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 发票信息 │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ 发票号: F2024-001 │ │ │
│ │ │ 日期: 2024-01-15 │ │ │
│ │ │ 到期日: 2024-02-15 │ │ │
│ │ │ 金额: 1,250.00 SEK │ │ │
│ │ │ OCR: 7350012345678 │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 会计科目 │ │
│ │ 借方: 5460 - Kontorsmaterial 1,000.00 │ │
│ │ 借方: 2610 - Ingående moms 250.00 │ │
│ │ 贷方: 2440 - Leverantörsskulder 1,250.00 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ [编辑信息] [重新识别] [取消] [确认并导入到Fortnox] │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## UI设计规范
### 7.1 设计原则
**独立Web应用设计原则:**
1. **品牌一致性**: 保持Invoice Master品牌同时尊重Fortnox用户习惯
2. **简洁高效**: 发票处理是高频操作,界面必须简洁快速
3. **清晰反馈**: OCR识别结果必须清晰展示便于用户确认
4. **无缝集成**: 虽然是独立应用但要让用户感觉与Fortnox是一体的
### 7.2 响应式设计
**断点定义:**
| 断点 | 宽度 | 布局 |
|------|------|------|
| Mobile | < 768px | 单列堆叠布局 |
| Tablet | 768px - 1024px | 双列布局 |
| Desktop | > 1024px | 三列布局 |
### 7.3 组件规范
#### 文件上传区域
```
┌─────────────────────────────────────────────────────────────┐
│ 📤 上传发票 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 📄 拖放PDF文件到这里 │ │
│ │ │ │
│ │ 或点击选择 │ │
│ │ │ │
│ │ 支持格式: PDF, JPG, PNG (最大10MB) │ │
│ │ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
**交互状态:**
- 默认: 灰色边框,虚线
- 悬停: 蓝色边框,背景变浅蓝
- 拖入: 蓝色边框,背景变深蓝
- 上传中: 显示进度条
#### 发票卡片
```
┌─────────────────────────────────────────────────────────────┐
│ 📄 Invoice_F2024_001.pdf [✓] [✏️] [🗑️]│
├─────────────────────────────────────────────────────────────┤
│ │
│ 供应商: ABC Company (556677-8899) │
│ 金额: 1,250.00 SEK │
│ 日期: 2024-01-15 │
│ │
│ 状态: ✅ 已导入到Fortnox │
│ 凭证: A-1234 [在Fortnox中查看] │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 7.4 颜色规范
**主色调:**
| 用途 | 颜色 | Hex |
|------|------|-----|
| 主色 | 蓝色 | #2563EB |
| 成功 | 绿色 | #10B981 |
| 警告 | 黄色 | #F59E0B |
| 错误 | 红色 | #EF4444 |
| 背景 | 浅灰 | #F9FAFB |
| 文字 | 深灰 | #1F2937 |
**Fortnox品牌协调:**
- 使用Fortnox的蓝色作为次要色 (#0057FF)
- 在"导入到Fortnox"按钮中使用Fortnox品牌色
### 7.5 字体规范
| 元素 | 字体 | 大小 | 字重 |
|------|------|------|------|
| 标题 | Inter | 24px | 600 |
| 副标题 | Inter | 18px | 500 |
| 正文 | Inter | 14px | 400 |
| 小字 | Inter | 12px | 400 |
| 数字 | Inter | 16px | 600 (等宽) |
---
## API设计
### 8.1 REST API端点
#### 认证相关
```http
# 获取Fortnox授权URL
GET /api/v1/fortnox/auth/url
Response: {
"authorization_url": "https://apps.fortnox.se/oauth-v1/auth?...",
"state": "random_state_string"
}
# OAuth回调处理
GET /api/v1/fortnox/auth/callback?code=xxx&state=xxx
Response: {
"status": "success",
"company_name": "My Company AB",
"connected_at": "2024-01-15T10:30:00Z"
}
# 断开连接
DELETE /api/v1/fortnox/auth
Response: {
"status": "disconnected"
}
```
#### 发票处理
```http
# 上传并处理发票
POST /api/v1/fortnox/invoices
Content-Type: multipart/form-data
Body: {
"file": [PDF file],
"auto_import": false, // 是否自动导入false则返回预览
"settings": {
"voucher_series": "A",
"attach_pdf": true
}
}
Response (预览模式): {
"id": "uuid",
"status": "preview",
"extraction": {
"supplier_name": "ABC Company",
"supplier_org_number": "556677-8899",
"invoice_number": "F2024-001",
"invoice_date": "2024-01-15",
"amount_total": 1250.00,
"ocr_number": "7350012345678"
},
"supplier_match": {
"action": "USE_EXISTING",
"supplier_number": "123",
"confidence": 1.0
},
"voucher_preview": {
"rows": [...]
}
}
Response (自动导入模式): {
"id": "uuid",
"status": "imported",
"voucher": {
"voucher_number": "1234",
"series": "A",
"url": "https://api.fortnox.se/3/vouchers/A/1234"
},
"fortnox_url": "https://apps.fortnox.se/..."
}
```
#### 供应商管理
```http
# 获取Fortnox供应商列表
GET /api/v1/fortnox/suppliers
Response: {
"suppliers": [
{
"supplier_number": "123",
"name": "ABC Company",
"organisation_number": "556677-8899"
}
]
}
# 创建供应商
POST /api/v1/fortnox/suppliers
Body: {
"name": "New Supplier",
"organisation_number": "112233-4455",
"address": "..."
}
```
### 8.2 Webhook接收
```http
# Fortnox Webhook接收端点
POST /webhooks/fortnox
Headers: {
"X-Fortnox-Event": "voucher.created"
}
Body: {
"event": "voucher.created",
"data": {
"voucher_number": "1234",
"series": "A"
}
}
```
---
## 数据库设计
### 9.1 实体关系图
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ fortnox_tenants │ │ fortnox_invoices │ │ supplier_cache │
├─────────────────┤ ├──────────────────┤ ├─────────────────┤
│ id (PK) │◄──────┤ id (PK) │ │ id (PK) │
│ organization_id │ │ tenant_id (FK) │ │ tenant_id (FK) │
│ access_token │ │ file_path │ │ supplier_number │
│ refresh_token │ │ extraction_data │ │ name │
│ expires_at │ │ voucher_id │ │ org_number │
│ company_name │ │ status │ │ cached_at │
│ created_at │ │ created_at │ └─────────────────┘
└─────────────────┘ └──────────────────┘
│ ┌──────────────────┐
│ │ processing_queue │
│ ├──────────────────┤
└────────►│ id (PK) │
│ invoice_id (FK) │
│ status │
│ retry_count │
└──────────────────┘
```
### 9.2 SQL Schema
```sql
-- Fortnox租户表
CREATE TABLE fortnox_tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id),
-- OAuth Tokens
access_token TEXT NOT NULL,
refresh_token TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL,
scope TEXT,
-- 公司信息
company_name VARCHAR(255),
company_org_number VARCHAR(20),
-- 设置
default_voucher_series VARCHAR(10) DEFAULT 'A',
default_account_code INTEGER DEFAULT 5460,
auto_attach_pdf BOOLEAN DEFAULT true,
-- 状态
is_active BOOLEAN DEFAULT true,
last_sync_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(organization_id)
);
-- Fortnox发票处理记录
CREATE TABLE fortnox_invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES fortnox_tenants(id),
-- 文件信息
original_filename VARCHAR(255),
storage_path TEXT,
file_size INTEGER,
-- OCR提取结果
extraction_data JSONB,
extraction_confidence DECIMAL(3,2),
-- 供应商匹配
supplier_number VARCHAR(50),
supplier_match_confidence DECIMAL(3,2),
supplier_match_action VARCHAR(20), -- USE_EXISTING, CREATE_NEW, SUGGEST_MATCH
-- Fortnox凭证
voucher_series VARCHAR(10),
voucher_number VARCHAR(50),
voucher_url TEXT,
-- 处理状态
status VARCHAR(20) DEFAULT 'pending', -- pending, processing, preview, imported, failed
error_message TEXT,
-- 用户操作
reviewed_by UUID,
reviewed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- 供应商缓存
CREATE TABLE supplier_cache (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES fortnox_tenants(id),
supplier_number VARCHAR(50) NOT NULL,
name VARCHAR(255),
organisation_number VARCHAR(20),
address TEXT,
phone VARCHAR(50),
email VARCHAR(255),
cached_at TIMESTAMP DEFAULT NOW(),
UNIQUE(tenant_id, supplier_number)
);
-- 处理队列
CREATE TABLE processing_queue (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
invoice_id UUID NOT NULL REFERENCES fortnox_invoices(id),
status VARCHAR(20) DEFAULT 'queued', -- queued, processing, completed, failed
priority INTEGER DEFAULT 5,
retry_count INTEGER DEFAULT 0,
max_retries INTEGER DEFAULT 3,
scheduled_at TIMESTAMP DEFAULT NOW(),
started_at TIMESTAMP,
completed_at TIMESTAMP,
error_message TEXT
);
-- 索引
CREATE INDEX idx_fortnox_invoices_tenant ON fortnox_invoices(tenant_id);
CREATE INDEX idx_fortnox_invoices_status ON fortnox_invoices(status);
CREATE INDEX idx_supplier_cache_tenant ON supplier_cache(tenant_id);
CREATE INDEX idx_processing_queue_status ON processing_queue(status);
```
---
## 安全设计
### 10.1 认证安全
**Token存储:**
- Access Token和Refresh Token使用AES-256加密存储
- 加密密钥存储在Azure Key Vault / AWS Secrets Manager
- Token定期轮换
**OAuth安全:**
- 使用state参数防止CSRF攻击
- 强制HTTPS回调
- 授权码一次性使用
### 10.2 数据安全
**传输安全:**
- 所有API通信强制TLS 1.3
- 证书固定(Certificate Pinning)防止中间人攻击
**存储安全:**
- 发票PDF加密存储(AES-256)
- 数据库连接使用SSL
- 敏感字段加密(组织号、银行信息)
### 10.3 访问控制
```python
# 权限检查装饰器
async def require_fortnox_connection(func):
@wraps(func)
async def wrapper(request: Request, *args, **kwargs):
tenant_id = request.headers.get('X-Tenant-ID')
# 检查是否已连接Fortnox
connection = await get_fortnox_connection(tenant_id)
if not connection or not connection.is_active:
raise HTTPException(
status_code=401,
detail="Fortnox connection required"
)
# 检查Token是否有效
if connection.is_token_expired():
await refresh_fortnox_token(connection)
return await func(request, *args, **kwargs)
return wrapper
```
---
## 错误处理
### 11.1 错误分类
| 错误类型 | 示例 | 处理策略 |
|---------|------|----------|
| **认证错误** | Token过期、无效 | 自动刷新或提示重新授权 |
| **API限制** | 429 Too Many Requests | 指数退避重试 |
| **数据错误** | 无效的组织号格式 | 返回具体验证错误 |
| **网络错误** | 连接超时 | 重试3次后失败 |
| **业务错误** | 供应商不存在 | 提供创建选项 |
### 11.2 错误响应格式
```json
{
"error": {
"code": "FORTNOX_TOKEN_EXPIRED",
"message": "Fortnox access token has expired",
"details": {
"action": "RECONNECT_REQUIRED",
"reconnect_url": "/api/v1/fortnox/auth/url"
},
"timestamp": "2024-01-15T10:30:00Z",
"request_id": "req_123456"
}
}
```
### 11.3 重试策略
```python
class FortnoxAPIRetry:
"""Fortnox API重试策略"""
def __init__(self):
self.max_retries = 3
self.base_delay = 1 # 秒
async def execute(self, func, *args, **kwargs):
for attempt in range(self.max_retries):
try:
return await func(*args, **kwargs)
except FortnoxAPIError as e:
if e.status_code == 429: # Rate limit
delay = self.base_delay * (2 ** attempt)
await asyncio.sleep(delay)
elif e.status_code in [500, 502, 503, 504]:
if attempt < self.max_retries - 1:
delay = self.base_delay * (2 ** attempt)
await asyncio.sleep(delay)
else:
raise
else:
raise
```
---
## 开发计划
### 12.1 里程碑
| 阶段 | 时间 | 目标 | 交付物 |
|------|------|------|--------|
| **M1** | Week 1-2 | 基础架构 | 认证模块、数据库 |
| **M2** | Week 3-4 | 核心功能 | 发票处理、供应商匹配 |
| **M3** | Week 5-6 | Fortnox集成 | API集成、凭证创建 |
| **M4** | Week 7-8 | UI开发 | 前端界面、用户流程 |
| **M5** | Week 9-10 | 测试优化 | 测试、性能优化 |
| **M6** | Week 11-12 | 上线准备 | 文档、审核、部署 |
### 12.2 任务分解
**Week 1-2: 基础架构**
- [ ] 创建Fortnox开发者账号
- [ ] 设计数据库Schema
- [ ] 实现OAuth2认证流程
- [ ] Token管理和刷新机制
- [ ] 基础API客户端
**Week 3-4: 核心功能**
- [ ] 集成Invoice Master OCR
- [ ] 实现供应商匹配算法
- [ ] 文件上传和存储
- [ ] 异步处理队列
**Week 5-6: Fortnox集成**
- [ ] 供应商API集成
- [ ] 凭证创建逻辑
- [ ] 文件附件上传
- [ ] 错误处理和重试
**Week 7-8: UI开发**
- [ ] 连接设置页面
- [ ] 发票上传界面
- [ ] 结果预览/编辑页面
- [ ] 历史记录页面
**Week 9-10: 测试优化**
- [ ] 单元测试 (目标80%覆盖率)
- [ ] 集成测试
- [ ] 性能测试
- [ ] 安全审计
**Week 11-12: 上线准备**
- [ ] 用户文档
- [ ] API文档
- [ ] Fortnox审核申请
- [ ] 生产环境部署
---
## 测试策略
### 13.1 测试类型
| 测试类型 | 工具 | 覆盖率目标 | 说明 |
|---------|------|-----------|------|
| **单元测试** | pytest | 80% | 核心逻辑 |
| **集成测试** | pytest + httpx | - | Fortnox API交互 |
| **E2E测试** | Playwright | 核心流程 | 用户场景 |
| **性能测试** | Locust | - | 并发处理 |
| **安全测试** | bandit, safety | - | 漏洞扫描 |
### 13.2 测试用例示例
```python
# 供应商匹配测试
class TestSupplierMatcher:
async def test_exact_org_number_match(self):
"""测试组织号精确匹配"""
matcher = FortnoxSupplierMatcher(mock_client)
result = await matcher.match(
tenant_id="test",
extraction=ExtractionResult(
supplier_org_number="556677-8899"
)
)
assert result.action == 'USE_EXISTING'
assert result.confidence == 1.0
async def test_fuzzy_name_match(self):
"""测试名称模糊匹配"""
result = await matcher.match(
tenant_id="test",
extraction=ExtractionResult(
supplier_name="ABC Company AB"
)
)
assert result.confidence > 0.85
# Fortnox API集成测试
class TestFortnoxIntegration:
async def test_create_voucher(self):
"""测试创建会计凭证"""
creator = FortnoxVoucherCreator(client)
result = await creator.create_voucher(
tenant_id="test",
extraction=mock_extraction,
supplier_number="123",
settings=mock_settings
)
assert result.voucher_id is not None
```
---
## 部署方案
### 14.1 架构部署图
```
┌─────────────────────────────────────────────────────────────┐
│ Azure │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Container │ │ PostgreSQL │ │ Blob │ │
│ │ Apps │ │ Flexible │ │ Storage │ │
│ │ (FastAPI) │ │ Server │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ Redis │ │
│ │ Cache │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 14.2 资源配置
**Container Apps:**
- CPU: 1 vCPU
- Memory: 2 GiB
- Min replicas: 1
- Max replicas: 5
**PostgreSQL:**
- SKU: Standard_B1ms
- Storage: 32 GB
- Backup: 7 days
**Blob Storage:**
- Tier: Hot
- Redundancy: LRS
### 14.3 部署流程
```bash
# 1. 基础设施部署
az group create --name invoice-master-rg --location swedencentral
# 2. 数据库部署
az postgres flexible-server create \
--name invoice-master-db \
--resource-group invoice-master-rg \
--sku-name Standard_B1ms
# 3. 应用部署
az containerapp create \
--name invoice-master-fortnox \
--resource-group invoice-master-rg \
--image invoicemaster.azurecr.io/fortnox-integration:latest \
--cpu 1 --memory 2Gi \
--min-replicas 1 --max-replicas 5
```
---
## 附录
### A. Fortnox API参考
**常用端点速查:**
| 功能 | 方法 | 端点 |
|------|------|------|
| 获取公司信息 | GET | /3/companyinformation |
| 获取供应商列表 | GET | /3/suppliers |
| 创建供应商 | POST | /3/suppliers |
| 获取会计科目 | GET | /3/accounts |
| 创建凭证 | POST | /3/vouchers |
| 上传文件 | POST | /3/inbox |
### B. 科目表参考 (BAS2024)
**常用费用科目:**
| 代码 | 名称 | 说明 |
|------|------|------|
| 2440 | Leverantörsskulder | 应付账款 |
| 2610 | Ingående moms | 进项VAT 25% |
| 2620 | Ingående moms | 进项VAT 12% |
| 2630 | Ingående moms | 进项VAT 6% |
| 5460 | Kontorsmaterial | 办公用品 |
| 5710 | Frakter | 运输费 |
| 6100 | Övriga externa tjänster | 其他外部服务 |
| 6210 | Konsultarvoden | 咨询费 |
### C. 错误代码表
| 代码 | 说明 | HTTP状态 |
|------|------|---------|
| FORTNOX_TOKEN_EXPIRED | Token过期 | 401 |
| FORTNOX_RATE_LIMITED | 请求过于频繁 | 429 |
| SUPPLIER_NOT_FOUND | 供应商不存在 | 404 |
| INVALID_ORG_NUMBER | 无效的组织号 | 400 |
| EXTRACTION_FAILED | OCR提取失败 | 422 |
### D. 相关链接
- [Fortnox Developer Portal](https://developer.fortnox.se/)
- [Fortnox API Docs](https://api.fortnox.se/apidocs)
- [BAS Kontoplan](https://www.bas.se/)
---
**文档版本历史:**
| 版本 | 日期 | 作者 | 变更说明 |
|------|------|------|---------|
| 1.0 | 2026-02-01 | Claude Code | 初始版本添加Fortnox集成模式说明和UI设计规范 |
---
**审批:**
- [ ] 技术负责人
- [ ] 产品经理
- [ ] 安全团队