1691 lines
62 KiB
Markdown
1691 lines
62 KiB
Markdown
# 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设计规范 |
|
||
|
||
---
|
||
|
||
**审批:**
|
||
|
||
- [ ] 技术负责人
|
||
- [ ] 产品经理
|
||
- [ ] 安全团队
|