feat: 凭证生成、成本分析、AI问答、前端页面、集成测试与E2E测试
- 后端: 凭证生成引擎、金蝶导出器、凭证模板服务 - 后端: 成本分析服务、AI问答服务 - 后端: 科目映射CRUD API、分析API、QA API - 后端: 集成测试(认证/任务/凭证) 49个测试全部通过 - 前端: 凭证管理、成本分析、导出中心、知识库、系统设置页面 - 前端: AuthGuard认证守卫、Dashboard AI聊天功能 - 前端: Playwright E2E测试 16 passed, 1 skipped - 基础设施: Docker Compose、Nginx反向代理、.env.example - 文档: 用户手册、管理员手册、发布检查清单
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
金蝶格式导出服务
|
||||
|
||||
将凭证导出为金蝶K3/星空可导入的格式
|
||||
"""
|
||||
|
||||
import csv
|
||||
import io
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.voucher import Voucher
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KingdeeExporterService:
|
||||
"""金蝶格式导出服务"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def export_voucher(
|
||||
self,
|
||||
voucher_id: int,
|
||||
format_type: str = "csv",
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出凭证为金蝶格式
|
||||
|
||||
Args:
|
||||
voucher_id: 凭证ID
|
||||
format_type: 导出格式 (csv/excel)
|
||||
|
||||
Returns:
|
||||
StreamingResponse
|
||||
"""
|
||||
voucher = await self.db.get(Voucher, voucher_id)
|
||||
if not voucher:
|
||||
raise ValueError(f"凭证不存在: {voucher_id}")
|
||||
|
||||
if format_type == "excel":
|
||||
return self._export_excel(voucher)
|
||||
else:
|
||||
return self._export_csv(voucher)
|
||||
|
||||
async def export_task_vouchers(
|
||||
self,
|
||||
task_id: int,
|
||||
format_type: str = "csv",
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出任务的所有凭证
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
format_type: 导出格式
|
||||
|
||||
Returns:
|
||||
StreamingResponse
|
||||
"""
|
||||
result = await self.db.execute(
|
||||
select(Voucher).where(Voucher.task_id == task_id)
|
||||
)
|
||||
vouchers = list(result.scalars().all())
|
||||
|
||||
if not vouchers:
|
||||
raise ValueError(f"任务 {task_id} 无凭证")
|
||||
|
||||
if len(vouchers) == 1:
|
||||
return await self.export_voucher(vouchers[0].id, format_type)
|
||||
|
||||
# 多凭证导出
|
||||
if format_type == "excel":
|
||||
return self._export_multiple_excel(vouchers)
|
||||
else:
|
||||
return self._export_multiple_csv(vouchers)
|
||||
|
||||
def _export_csv(self, voucher: Voucher) -> StreamingResponse:
|
||||
"""
|
||||
金蝶K3 CSV导入格式
|
||||
|
||||
格式: 凭证日期, 凭证号, 摘要, 科目代码, 科目名称, 借方金额, 贷方金额, 制单人
|
||||
"""
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
|
||||
# 金蝶K3导入格式头
|
||||
writer.writerow([
|
||||
"凭证日期", "凭证字", "凭证号", "摘要",
|
||||
"科目代码", "科目名称",
|
||||
"借方金额", "贷方金额",
|
||||
"制单人", "审核人",
|
||||
])
|
||||
|
||||
entries = voucher.entries or []
|
||||
for i, entry in enumerate(entries):
|
||||
writer.writerow([
|
||||
voucher.voucher_date,
|
||||
"记",
|
||||
voucher.voucher_number,
|
||||
entry.get("summary", voucher.summary),
|
||||
entry.get("account_code", ""),
|
||||
entry.get("account_name", ""),
|
||||
f'{entry.get("debit_amount", 0):.2f}',
|
||||
f'{entry.get("credit_amount", 0):.2f}',
|
||||
"AI助手",
|
||||
"",
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
|
||||
# 转为 bytes
|
||||
content = output.getvalue().encode("utf-8-sig") # BOM for Excel compatibility
|
||||
return StreamingResponse(
|
||||
io.BytesIO(content),
|
||||
media_type="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="voucher_{voucher.voucher_number}.csv"',
|
||||
},
|
||||
)
|
||||
|
||||
def _export_excel(self, voucher: Voucher) -> StreamingResponse:
|
||||
"""导出为 Excel 格式"""
|
||||
from openpyxl import Workbook
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "凭证"
|
||||
|
||||
# 标题行
|
||||
ws.append(["凭证编号", voucher.voucher_number])
|
||||
ws.append(["凭证日期", voucher.voucher_date])
|
||||
ws.append(["会计期间", voucher.period])
|
||||
ws.append(["摘要", voucher.summary])
|
||||
ws.append([])
|
||||
|
||||
# 分录表头
|
||||
ws.append(["序号", "科目代码", "科目名称", "摘要", "借方金额", "贷方金额"])
|
||||
|
||||
entries = voucher.entries or []
|
||||
for i, entry in enumerate(entries, 1):
|
||||
ws.append([
|
||||
i,
|
||||
entry.get("account_code", ""),
|
||||
entry.get("account_name", ""),
|
||||
entry.get("summary", ""),
|
||||
entry.get("debit_amount", 0),
|
||||
entry.get("credit_amount", 0),
|
||||
])
|
||||
|
||||
# 合计行
|
||||
ws.append([])
|
||||
ws.append(["", "", "", "合计", voucher.total_debit, voucher.total_credit])
|
||||
|
||||
output = io.BytesIO()
|
||||
wb.save(output)
|
||||
output.seek(0)
|
||||
|
||||
return StreamingResponse(
|
||||
output,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="voucher_{voucher.voucher_number}.xlsx"',
|
||||
},
|
||||
)
|
||||
|
||||
def _export_multiple_csv(self, vouchers: List[Voucher]) -> StreamingResponse:
|
||||
"""多凭证 CSV 导出"""
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(writer)
|
||||
|
||||
writer.writerow([
|
||||
"凭证日期", "凭证字", "凭证号", "摘要",
|
||||
"科目代码", "科目名称",
|
||||
"借方金额", "贷方金额",
|
||||
])
|
||||
|
||||
for voucher in vouchers:
|
||||
entries = voucher.entries or []
|
||||
for entry in entries:
|
||||
writer.writerow([
|
||||
voucher.voucher_date,
|
||||
"记",
|
||||
voucher.voucher_number,
|
||||
entry.get("summary", voucher.summary),
|
||||
entry.get("account_code", ""),
|
||||
entry.get("account_name", ""),
|
||||
f'{entry.get("debit_amount", 0):.2f}',
|
||||
f'{entry.get("credit_amount", 0):.2f}',
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
content = output.getvalue().encode("utf-8-sig")
|
||||
return StreamingResponse(
|
||||
io.BytesIO(content),
|
||||
media_type="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="vouchers_batch.csv"',
|
||||
},
|
||||
)
|
||||
|
||||
def _export_multiple_excel(self, vouchers: List[Voucher]) -> StreamingResponse:
|
||||
"""多凭证 Excel 导出"""
|
||||
from openpyxl import Workbook
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "凭证汇总"
|
||||
|
||||
ws.append([
|
||||
"凭证日期", "凭证号", "会计期间", "摘要",
|
||||
"科目代码", "科目名称",
|
||||
"借方金额", "贷方金额",
|
||||
])
|
||||
|
||||
for voucher in vouchers:
|
||||
entries = voucher.entries or []
|
||||
for entry in entries:
|
||||
ws.append([
|
||||
voucher.voucher_date,
|
||||
voucher.voucher_number,
|
||||
voucher.period,
|
||||
entry.get("summary", voucher.summary),
|
||||
entry.get("account_code", ""),
|
||||
entry.get("account_name", ""),
|
||||
entry.get("debit_amount", 0),
|
||||
entry.get("credit_amount", 0),
|
||||
])
|
||||
|
||||
output = io.BytesIO()
|
||||
wb.save(output)
|
||||
output.seek(0)
|
||||
|
||||
return StreamingResponse(
|
||||
output,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="vouchers_batch.xlsx"',
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user