Files
selfrelease 5278190750 feat: 凭证生成、成本分析、AI问答、前端页面、集成测试与E2E测试
- 后端: 凭证生成引擎、金蝶导出器、凭证模板服务
- 后端: 成本分析服务、AI问答服务
- 后端: 科目映射CRUD API、分析API、QA API
- 后端: 集成测试(认证/任务/凭证) 49个测试全部通过
- 前端: 凭证管理、成本分析、导出中心、知识库、系统设置页面
- 前端: AuthGuard认证守卫、Dashboard AI聊天功能
- 前端: Playwright E2E测试 16 passed, 1 skipped
- 基础设施: Docker Compose、Nginx反向代理、.env.example
- 文档: 用户手册、管理员手册、发布检查清单
2026-07-07 21:21:29 +08:00

267 lines
8.1 KiB
Python

"""
凭证 API
提供凭证生成、查询、确认和金蝶导出接口
"""
import io
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.tenant import get_current_company_id
from app.models.voucher import Voucher, VoucherStatus
from app.models.account_mapping import AccountMapping
from app.services.voucher.generator import VoucherGeneratorService
from app.services.voucher.kingdee_exporter import KingdeeExporterService
from app.services.voucher.template import VoucherTemplate
router = APIRouter(prefix="/api/vouchers", tags=["凭证管理"])
class VoucherEntryResponse(BaseModel):
"""凭证分录响应"""
account_code: str
account_name: str
debit_amount: float
credit_amount: float
summary: str
department: str = ""
class VoucherResponse(BaseModel):
"""凭证响应"""
id: int
voucher_number: str
voucher_date: str
period: str
summary: str
entries: List[Dict[str, Any]]
total_debit: float
total_credit: float
status: str
confirmed_by: Optional[int] = None
confirmed_at: Optional[str] = None
created_at: str
updated_at: str
class GenerateVoucherRequest(BaseModel):
"""生成凭证请求"""
task_id: int
period: str = ""
class ConfirmVoucherRequest(BaseModel):
"""确认凭证请求"""
user_id: int
class AccountMappingResponse(BaseModel):
"""科目映射响应"""
id: int
standard_field: str
debit_account: str
debit_account_name: str
credit_account: str
credit_account_name: str
cost_center: Optional[str] = None
is_active: bool
class AccountMappingRequest(BaseModel):
"""科目映射请求"""
standard_field: str
debit_account: str
debit_account_name: str
credit_account: str
credit_account_name: str
cost_center: Optional[str] = None
@router.post("/generate", response_model=VoucherResponse)
async def generate_voucher(
request: GenerateVoucherRequest,
db: AsyncSession = Depends(get_db),
company_id: int = Depends(get_current_company_id),
) -> VoucherResponse:
"""生成会计凭证"""
service = VoucherGeneratorService(db)
# 检查是否已有凭证
existing = await service.get_voucher(request.task_id)
if existing:
return _to_response(existing)
voucher = await service.generate_voucher(
task_id=request.task_id,
company_id=company_id,
period=request.period,
)
return _to_response(voucher)
@router.get("/task/{task_id}", response_model=Optional[VoucherResponse])
async def get_task_voucher(
task_id: int,
db: AsyncSession = Depends(get_db),
company_id: int = Depends(get_current_company_id),
) -> Optional[VoucherResponse]:
"""获取任务的凭证"""
service = VoucherGeneratorService(db)
voucher = await service.get_voucher(task_id)
if not voucher:
return None
return _to_response(voucher)
@router.get("/{voucher_id}", response_model=VoucherResponse)
async def get_voucher(
voucher_id: int,
db: AsyncSession = Depends(get_db),
company_id: int = Depends(get_current_company_id),
) -> VoucherResponse:
"""获取凭证详情"""
voucher = await db.get(Voucher, voucher_id)
if not voucher:
raise HTTPException(status_code=404, detail="凭证不存在")
return _to_response(voucher)
@router.post("/{voucher_id}/confirm", response_model=VoucherResponse)
async def confirm_voucher(
voucher_id: int,
request: ConfirmVoucherRequest,
db: AsyncSession = Depends(get_db),
company_id: int = Depends(get_current_company_id),
) -> VoucherResponse:
"""确认凭证"""
service = VoucherGeneratorService(db)
voucher = await service.confirm_voucher(voucher_id, request.user_id)
if not voucher:
raise HTTPException(status_code=404, detail="凭证不存在")
return _to_response(voucher)
@router.get("/task/{task_id}/export")
async def export_voucher(
task_id: int,
format: str = Query("csv", description="导出格式: csv 或 excel"),
db: AsyncSession = Depends(get_db),
company_id: int = Depends(get_current_company_id),
) -> StreamingResponse:
"""导出凭证为金蝶格式"""
service = KingdeeExporterService(db)
try:
return await service.export_task_vouchers(task_id, format)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
# ===== 科目映射管理 =====
@router.get("/account-mappings/list", response_model=List[AccountMappingResponse])
async def list_account_mappings(
db: AsyncSession = Depends(get_db),
company_id: int = Depends(get_current_company_id),
) -> List[AccountMappingResponse]:
"""获取科目映射列表"""
result = await db.execute(
select(AccountMapping).where(AccountMapping.company_id == company_id)
)
mappings = result.scalars().all()
return [AccountMappingResponse(**m.__dict__) for m in mappings]
@router.post("/account-mappings", response_model=AccountMappingResponse)
async def create_account_mapping(
request: AccountMappingRequest,
db: AsyncSession = Depends(get_db),
company_id: int = Depends(get_current_company_id),
) -> AccountMappingResponse:
"""创建科目映射"""
mapping = AccountMapping(
company_id=company_id,
standard_field=request.standard_field,
debit_account=request.debit_account,
debit_account_name=request.debit_account_name,
credit_account=request.credit_account,
credit_account_name=request.credit_account_name,
cost_center=request.cost_center,
is_active=True,
)
db.add(mapping)
await db.commit()
await db.refresh(mapping)
return AccountMappingResponse(**mapping.__dict__)
@router.put("/account-mappings/{mapping_id}", response_model=AccountMappingResponse)
async def update_account_mapping(
mapping_id: int,
request: AccountMappingRequest,
db: AsyncSession = Depends(get_db),
company_id: int = Depends(get_current_company_id),
) -> AccountMappingResponse:
"""更新科目映射"""
mapping = await db.get(AccountMapping, mapping_id)
if not mapping:
raise HTTPException(status_code=404, detail="科目映射不存在")
mapping.standard_field = request.standard_field
mapping.debit_account = request.debit_account
mapping.debit_account_name = request.debit_account_name
mapping.credit_account = request.credit_account
mapping.credit_account_name = request.credit_account_name
mapping.cost_center = request.cost_center
await db.commit()
await db.refresh(mapping)
return AccountMappingResponse(**mapping.__dict__)
@router.delete("/account-mappings/{mapping_id}")
async def delete_account_mapping(
mapping_id: int,
db: AsyncSession = Depends(get_db),
company_id: int = Depends(get_current_company_id),
) -> Dict[str, str]:
"""删除科目映射"""
mapping = await db.get(AccountMapping, mapping_id)
if not mapping:
raise HTTPException(status_code=404, detail="科目映射不存在")
await db.delete(mapping)
await db.commit()
return {"message": "已删除"}
@router.get("/templates/list")
async def get_voucher_templates() -> Dict[str, Any]:
"""获取默认凭证模板"""
return VoucherTemplate.get_all_templates()
def _to_response(voucher: Voucher) -> VoucherResponse:
"""转换凭证模型为响应"""
return VoucherResponse(
id=voucher.id,
voucher_number=voucher.voucher_number,
voucher_date=voucher.voucher_date,
period=voucher.period,
summary=voucher.summary,
entries=voucher.entries or [],
total_debit=voucher.total_debit,
total_credit=voucher.total_credit,
status=voucher.status,
confirmed_by=voucher.confirmed_by,
confirmed_at=voucher.confirmed_at.isoformat() if voucher.confirmed_at else None,
created_at=voucher.created_at.isoformat() if voucher.created_at else "",
updated_at=voucher.updated_at.isoformat() if voucher.updated_at else "",
)