62 KiB
Invoice Master - Fortnox Integration Technical Specification
版本: v1.0
日期: 2026-02-01
作者: Claude Code
状态: 设计阶段
目录
概述
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 供应商管理
# 获取供应商列表
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)
# 创建会计凭证
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 文件上传
# 上传附件到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% | 交通等 |
科目选择逻辑:
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 供应商匹配算法
匹配优先级:
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存储和刷新
- 多租户隔离
核心类:
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)
处理流程:
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)
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)
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应用设计原则:
- 品牌一致性: 保持Invoice Master品牌,同时尊重Fortnox用户习惯
- 简洁高效: 发票处理是高频操作,界面必须简洁快速
- 清晰反馈: OCR识别结果必须清晰展示,便于用户确认
- 无缝集成: 虽然是独立应用,但要让用户感觉与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端点
认证相关
# 获取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"
}
发票处理
# 上传并处理发票
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/..."
}
供应商管理
# 获取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接收
# 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
-- 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 访问控制
# 权限检查装饰器
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 错误响应格式
{
"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 重试策略
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 测试用例示例
# 供应商匹配测试
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 部署流程
# 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. 相关链接
文档版本历史:
| 版本 | 日期 | 作者 | 变更说明 |
|---|---|---|---|
| 1.0 | 2026-02-01 | Claude Code | 初始版本,添加Fortnox集成模式说明和UI设计规范 |
审批:
- 技术负责人
- 产品经理
- 安全团队